# Busca manual + Ocorrência "sem código de barras" — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Permitir, na Alocação WMS, buscar produto por descrição quando não há código de barras, e registrar uma ocorrência "produto sem código de barras" que, ao concluir a missão, gera uma missão `AJUSTE_DADOS` executável para atribuir o código depois.

**Architecture:** Reusa o sinal `SugestoesCodigoBarras` (tabela `missao_codigo_sugestao` → `GerarAjusteDeSugestoesLaboratorio` → `AJUSTE_DADOS`). Estende-o com um `tipo='ausencia'` (sem código). Adiciona busca por descrição no dispatcher `AlocacaoWms`. Habilita o hook de finalização para `ALOCACAO_WMS`. O ramo `sugestao` (Separação/Carregamento, com código) fica intacto.

**Tech Stack:** PHP 8.x, `ModelInteligencia` (Sondas V2), PostgreSQL (`dev_gx_goldie` em dev), TddTest CLI (não-PHPUnit), front Metronic + JS pareado, i18n `show()`.

## Global Constraints

- Banco de dev é **`dev_gx_goldie`** (NUNCA `gx_goldie` = PROD). Todos os TddTests/integração rodam só nele.
- Escritas sempre via `update()`/`create()` do `ModelInteligencia` — sem SQL cru no runtime (`feedback_no_raw_sql`). SQL cru só em migrations e leituras pontuais já existentes.
- **Nunca** `git commit`/`push` sem pedido explícito do usuário (`feedback_no_autocommit`). Os passos "Commit" abaixo só são executados quando o usuário autorizar.
- Block de teste/sandbox = **22**; produto de catálogo real no block 22 = **16221**.
- Bootstrap dos TddTests: `$_SERVER['HTTP_HOST']='dev-genesis.goldieapp.com.br'`, `$_SERVER['REQUEST_METHOD']='GET'`, e `$_SESSION['galaxiaUserInfo']` **e** `$_SESSION['dashboard']['galaxiaUserInfo']` com `customerData->id=22`, `sectorData->inteligencia='dev_gx_goldie'`.
- i18n: textos PT literais via `show('texto')` (key=md5, auto-harvest).
- Colunas `block_*` são preenchidas pelo framework (`customerControl`); não declarar `DEFAULT` nem setar manualmente.

## File Structure

| Arquivo | Responsabilidade | Ação |
|---|---|---|
| `SugestoesCodigoBarras/Migrations/002_suporte_ausencia_codigo.sql` | Tornar `codigo_sugerido` NULLable + coluna `tipo` | Criar |
| `SugestoesCodigoBarras/Sondas/MissaoCodigoSugestaoSonda.php` | `registrarAusencia()`; tirar `codigo_sugerido` de `$required` | Modificar |
| `SugestoesCodigoBarras/Validators/SugerirCodigoBarrasValidator.php` | Ramo `tipo='ausencia'` (pula regras de código) | Modificar |
| `SugestoesCodigoBarras/Laboratorios/SinalizarSemCodigoLaboratorio.php` | Caso de uso "sinalizar sem código" | Criar |
| `AlocacaoWms/Laboratorios/BuscarProdutoLaboratorio.php` | Busca de produto por descrição/código interno | Criar |
| `AlocacaoWms/AlocacaoWms.php` | Ações `buscarProduto`, `sinalizarSemCodigo`, `cancelarSemCodigo` | Modificar |
| `SugestoesCodigoBarras/Adapters/AjusteDados/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorio.php` | Ramo `ausencia` → missão executável (itens pendentes) | Modificar |
| `SugestoesCodigoBarras/Laboratorios/GerarAjusteDeSugestoesLaboratorio.php` | Rótulo de origem `ALOCACAO_WMS` | Modificar |
| `Constelacoes/.../Satelites/MissoesStatus.php:452-464` | Whitelist do hook + `ALOCACAO_WMS` | Modificar |
| `Constelacoes/.../Satelites/MissoesAprovacao.php:515-524` | Espelho do hook + `ALOCACAO_WMS` | Modificar |
| `App/html/alocacaowms/item.php` + `item.js` | Busca manual + botão "sem código" | Modificar |
| `AlocacaoWms/Testes/...` + `SugestoesCodigoBarras/Testes/...` | TddTests | Criar |

---

### Task 1: Migration — `missao_codigo_sugestao` suporta ausência

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Migrations/002_suporte_ausencia_codigo.sql`
- Test: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Migrations/SuporteAusenciaSchemaTddTest.php`

**Interfaces:**
- Produces: coluna `tipo VARCHAR(20) NOT NULL DEFAULT 'sugestao'` e `codigo_sugerido` NULLable em `missao_codigo_sugestao`.

- [ ] **Step 1: Escrever a migration (idempotente, aditiva)**

`002_suporte_ausencia_codigo.sql`:
```sql
-- =====================================================
-- Migration: suportar ocorrência "sem código de barras"
-- Data: 2026-06-23
-- =====================================================
-- Caminho `ausencia`: operador sinaliza que o produto NÃO tem código de barras,
-- sem sugerir nenhum valor. `codigo_sugerido` passa a aceitar NULL; `tipo`
-- discrimina 'sugestao' (com código, fluxo legado) de 'ausencia' (sem código).
-- Aditiva e idempotente — linhas existentes ficam 'sugestao'.
-- =====================================================

ALTER TABLE IF EXISTS missao_codigo_sugestao
    ALTER COLUMN codigo_sugerido DROP NOT NULL;

ALTER TABLE IF EXISTS missao_codigo_sugestao
    ADD COLUMN IF NOT EXISTS tipo VARCHAR(20) NOT NULL DEFAULT 'sugestao';
```

- [ ] **Step 2: Escrever o teste de schema (vai falhar antes de aplicar)**

`SuporteAusenciaSchemaTddTest.php` (segue o padrão `MigrationSchemaTest`):
```php
<?php
if (php_sapi_name() !== 'cli') { die("Apenas via CLI\n"); }
$_SERVER['HTTP_HOST']      = 'dev-genesis.goldieapp.com.br';
$_SERVER['REQUEST_METHOD'] = 'GET';
require_once __DIR__ . '/../../../../../../../autoload.php';
if (session_status() === PHP_SESSION_NONE) { @session_start(); }
$info = new stdClass();
$info->userData         = (object)['id' => 1, 'username' => 'integ_test', 'name' => 'Integration Test'];
$info->customerData     = (object)['id' => 22];
$info->principalEmpresa = (object)['empresa_id' => 3];
$info->sectorData       = (object)['id' => 1, 'inteligencia' => 'dev_gx_goldie'];
$_SESSION['galaxiaUserInfo']              = $info;
$_SESSION['dashboard']['galaxiaUserInfo'] = $info;

use Galaxia\_Controladores\Connect;
$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') {
    fwrite(STDERR, "ABORT: não conectou em dev_gx_goldie\n"); exit(1);
}
$passou = 0; $falhas = 0;
function check(bool $c, string $m): void { global $passou,$falhas; if($c){$passou++;echo "[PASS] $m\n";}else{$falhas++;echo "[FAIL] $m\n";} }

$col = $pdo->query("SELECT is_nullable, data_type FROM information_schema.columns
                    WHERE table_name='missao_codigo_sugestao' AND column_name='codigo_sugerido'")->fetch(\PDO::FETCH_ASSOC);
check($col && $col['is_nullable'] === 'YES', 'codigo_sugerido é NULLable');

$tipo = $pdo->query("SELECT column_default, is_nullable FROM information_schema.columns
                     WHERE table_name='missao_codigo_sugestao' AND column_name='tipo'")->fetch(\PDO::FETCH_ASSOC);
check($tipo !== false, "coluna 'tipo' existe");
check($tipo && strpos((string)$tipo['column_default'], 'sugestao') !== false, "tipo default = 'sugestao'");

echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
exit($falhas > 0 ? 1 : 0);
```

- [ ] **Step 3: Rodar o teste e confirmar que FALHA**

Run: `php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Migrations/SuporteAusenciaSchemaTddTest.php`
Expected: FAIL (`tipo` não existe / `codigo_sugerido` NOT NULL).

- [ ] **Step 4: Aplicar a migration em `dev_gx_goldie`**

Run (exporte `PGPASSWORD` com a senha do `dev_gx_goldie` antes — não deixe a senha no doc):
```bash
PGPASSWORD="$PGPASSWORD" psql -h 147.93.9.197 -U postgres -d dev_gx_goldie \
  -f componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Migrations/002_suporte_ausencia_codigo.sql
```
Expected: `ALTER TABLE` ×2, sem erro.

- [ ] **Step 5: Rodar o teste e confirmar que PASSA**

Run: `php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Migrations/SuporteAusenciaSchemaTddTest.php`
Expected: `RESULTADO: PASS=3 FAIL=0`.

- [ ] **Step 6: Commit** *(somente se o usuário autorizar)*

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Migrations/002_suporte_ausencia_codigo.sql \
        componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Migrations/SuporteAusenciaSchemaTddTest.php
git commit -m "feat(sugestoes-codigo): schema suporta ocorrência sem código (tipo + codigo nullable)"
```

---

### Task 2: Sonda — `registrarAusencia()`

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Sondas/MissaoCodigoSugestaoSonda.php`
- Test: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Sondas/RegistrarAusenciaTddTest.php`

**Interfaces:**
- Consumes: tabela com `tipo` + `codigo_sugerido` NULLable (Task 1).
- Produces: `MissaoCodigoSugestaoSonda::registrarAusencia(int $origemId, int $produtoId, int $operadorId): ?object` — cria/mantém 1 pendência por (origem, produto) com `tipo='ausencia'`, `codigo_sugerido=NULL`. Idempotente.

- [ ] **Step 1: Escrever o teste (falha)**

`RegistrarAusenciaTddTest.php` (bootstrap padrão + Connect dev_gx_goldie como no Task 1; cria uma missão `ALOCACAO_WMS` temporária no block 22 pra servir de origem):
```php
// ... bootstrap idêntico ao Task 1 (HTTP_HOST, sessão, Connect dev_gx_goldie, check()) ...
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda;

$BLOCK = 22; $PRODUTO = 16221;
// origem: missão ALOCACAO_WMS (tipo 14) temporária
$pdo->exec("INSERT INTO missao_logistica (tipo_missao_id, status, descricao, modo_atribuicao, block, data_cadastro)
            VALUES (14, 'pendente', '__test_ausencia_origem', 'FILA_ABERTA', {$BLOCK}, NOW())");
$ORIGEM = (int)$pdo->query("SELECT id FROM missao_logistica WHERE descricao='__test_ausencia_origem' AND block={$BLOCK} ORDER BY id DESC LIMIT 1")->fetchColumn();

try {
    $sonda = new MissaoCodigoSugestaoSonda();
    $r = $sonda->registrarAusencia($ORIGEM, $PRODUTO, 1);
    check($r !== null, 'registrarAusencia retorna a linha criada');
    check(($r->tipo ?? '') === 'ausencia', "tipo = 'ausencia'");
    check(($r->codigo_sugerido ?? 'X') === null, 'codigo_sugerido NULL');
    check(($r->status ?? '') === 'pendente', 'status pendente');

    // idempotência por (origem, produto)
    $r2 = $sonda->registrarAusencia($ORIGEM, $PRODUTO, 1);
    $cnt = (int)$pdo->query("SELECT COUNT(*) FROM missao_codigo_sugestao
                             WHERE missao_origem_id={$ORIGEM} AND produto_id={$PRODUTO} AND status='pendente'")->fetchColumn();
    check($cnt === 1, 'idempotente: 1 pendência por (origem, produto)');
} finally {
    $pdo->exec("DELETE FROM missao_codigo_sugestao WHERE missao_origem_id={$ORIGEM}");
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$ORIGEM}");
}
echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
exit($falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar e confirmar FALHA**

Run: `php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Sondas/RegistrarAusenciaTddTest.php`
Expected: FAIL (método não existe).

- [ ] **Step 3: Implementar `registrarAusencia` + tirar `codigo_sugerido` de `$required`**

Em `MissaoCodigoSugestaoSonda::init()`, trocar a linha do `$required` (o validator é quem garante o código no fluxo `sugestao`; o model precisa aceitar NULL pro fluxo `ausencia`):
```php
        self::$required  = ['missao_origem_id', 'produto_id', 'operador_sugeriu_id'];
```

Adicionar o método (espelha `upsertPendente`, sem código, com `tipo`):
```php
    /**
     * Registra/mantém uma ocorrência "sem código de barras" pendente para o par
     * (origem, produto). Não exige código — `tipo='ausencia'`, `codigo_sugerido=NULL`.
     * Idempotente via o unique index parcial em status='pendente'.
     */
    public function registrarAusencia(int $origemId, int $produtoId, int $operadorId): ?object
    {
        $existente = $this->find(
            'missao_origem_id = :m AND produto_id = :p AND status = :s',
            "m={$origemId}&p={$produtoId}&s=" . StatusSugestao::PENDENTE->value
        )->fetch(false);

        if ($existente) {
            $this->update(
                ['tipo' => 'ausencia', 'codigo_sugerido' => null, 'operador_sugeriu_id' => $operadorId],
                'id = :id',
                "id={$existente->id}"
            );
            return $this->find('id = :id', "id={$existente->id}")->fetch(false);
        }

        $this->data = (object)[
            'missao_origem_id'    => $origemId,
            'produto_id'          => $produtoId,
            'codigo_sugerido'     => null,
            'tipo'                => 'ausencia',
            'operador_sugeriu_id' => $operadorId,
            'status'              => StatusSugestao::PENDENTE->value,
        ];
        $novoId = $this->create($this->safe());
        if (!$novoId) {
            error_log('[MissaoCodigoSugestaoSonda] registrarAusencia create falhou: '
                . ($this->fail() ? $this->fail()->getMessage() : 'erro desconhecido'));
            return null;
        }
        return $this->find('id = :id', "id={$novoId}")->fetch(false);
    }
```

- [ ] **Step 4: Rodar e confirmar PASSA**

Run: `php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Sondas/RegistrarAusenciaTddTest.php`
Expected: `RESULTADO: PASS=5 FAIL=0`. Se o insert falhar por `$required`/NULL, conferir que `codigo_sugerido` saiu do `$required` (Step 3).

- [ ] **Step 5: Rodar o teste existente da Sonda (não-regressão do fluxo `sugestao`)**

Run: `php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Sondas/MissaoCodigoSugestaoSondaTddTest.php`
Expected: continua PASS (upsertPendente com código segue funcionando).

- [ ] **Step 6: Commit** *(se autorizado)*

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Sondas/MissaoCodigoSugestaoSonda.php \
        componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Sondas/RegistrarAusenciaTddTest.php
git commit -m "feat(sugestoes-codigo): Sonda registrarAusencia (ocorrência sem código)"
```

---

### Task 3: Validator — ramo `tipo='ausencia'`

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Validators/SugerirCodigoBarrasValidator.php`
- Test: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Validators/SinalizarAusenciaValidatorTddTest.php`

**Interfaces:**
- Consumes: `validar(array $data, int $blockAtual): array`.
- Produces: quando `$data['tipo']==='ausencia'`, retorna `['ok'=>true,'data'=>['missaoId','produtoId','tipo'=>'ausencia']]` SEM exigir `codigo_sugerido`; mantém regras de missão (existe/ativa/tipo permitido) e de produto (existe e **sem** `codigo_barras`).

- [ ] **Step 1: Escrever o teste (falha)**

`SinalizarAusenciaValidatorTddTest.php` (bootstrap padrão; cria missão `ALOCACAO_WMS` ativa block 22; usa produto 16221 — garantir que está **sem** `codigo_barras` no dev, senão escolher/forjar um sem código):
```php
// ... bootstrap + Connect dev_gx_goldie + check() ...
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Validators\SugerirCodigoBarrasValidator;

$BLOCK = 22;
// produto sem código de barras (garante a precondição)
$PRODUTO = (int)$pdo->query("SELECT id FROM produtos WHERE block={$BLOCK} AND (codigo_barras IS NULL OR codigo_barras='') LIMIT 1")->fetchColumn();
if (!$PRODUTO) { fwrite(STDERR, "ABORT: sem produto sem código no block {$BLOCK}\n"); exit(1); }

$pdo->exec("INSERT INTO missao_logistica (tipo_missao_id, status, descricao, modo_atribuicao, block, data_cadastro)
            VALUES (14, 'pendente', '__test_ausencia_val', 'FILA_ABERTA', {$BLOCK}, NOW())");
$ORIGEM = (int)$pdo->query("SELECT id FROM missao_logistica WHERE descricao='__test_ausencia_val' AND block={$BLOCK} ORDER BY id DESC LIMIT 1")->fetchColumn();

try {
    $v = new SugerirCodigoBarrasValidator();
    // ausência aceita SEM código
    $r = $v->validar(['missao_origem_id'=>$ORIGEM, 'produto_id'=>$PRODUTO, 'tipo'=>'ausencia'], $BLOCK);
    check(($r['ok'] ?? false) === true, 'ausencia: aceita sem código');
    check(($r['data']['tipo'] ?? '') === 'ausencia', 'ausencia: data.tipo = ausencia');

    // ausência NÃO exige >=6 chars (campo de código ausente é ok)
    $r = $v->validar(['missao_origem_id'=>$ORIGEM, 'produto_id'=>$PRODUTO, 'tipo'=>'ausencia', 'codigo_sugerido'=>''], $BLOCK);
    check(($r['ok'] ?? false) === true, 'ausencia: ignora regra de tamanho mínimo');

    // produto que JÁ tem código → rejeita mesmo em ausencia
    $COMCODIGO = (int)$pdo->query("SELECT id FROM produtos WHERE block={$BLOCK} AND codigo_barras IS NOT NULL AND codigo_barras<>'' LIMIT 1")->fetchColumn();
    if ($COMCODIGO) {
        $r = $v->validar(['missao_origem_id'=>$ORIGEM, 'produto_id'=>$COMCODIGO, 'tipo'=>'ausencia'], $BLOCK);
        check(($r['ok'] ?? true) === false, 'ausencia: produto que já tem código → rejeita');
    }

    // fluxo sugestao (sem tipo) segue exigindo código >=6
    $r = $v->validar(['missao_origem_id'=>$ORIGEM, 'produto_id'=>$PRODUTO, 'codigo_sugerido'=>'123'], $BLOCK);
    check(($r['ok'] ?? true) === false, 'sugestao: código curto ainda rejeitado (não-regressão)');
} finally {
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$ORIGEM}");
}
echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
exit($falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar e confirmar FALHA**

Run: `php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Validators/SinalizarAusenciaValidatorTddTest.php`
Expected: FAIL (ausência hoje cai na regra de tamanho mínimo → `ok=false`).

- [ ] **Step 3: Implementar o ramo `ausencia`**

Em `SugerirCodigoBarrasValidator::validar`, logo após ler os campos (linha ~29), inserir o desvio:
```php
        $missaoId  = (int) ($data['missao_origem_id'] ?? 0);
        $produtoId = (int) ($data['produto_id'] ?? 0);
        $tipo      = ($data['tipo'] ?? 'sugestao') === 'ausencia' ? 'ausencia' : 'sugestao';
        $codigo    = trim((string) ($data['codigo_sugerido'] ?? ''));

        // 1. Obrigatórios
        if ($missaoId <= 0)  return self::erro('Missão de origem inválida');
        if ($produtoId <= 0) return self::erro('Produto inválido');

        // 1b. Regra de código só no fluxo 'sugestao'
        if ($tipo === 'sugestao' && strlen($codigo) < self::TAMANHO_MINIMO) {
            return self::erro('Mínimo de ' . self::TAMANHO_MINIMO . ' caracteres');
        }
```

Manter os blocos 2 (missão existe/tipo/não finalizada) e 3 (produto existe e **sem** `codigo_barras`) **iguais**. Envolver os blocos 4 e 5 (dedupe por código / outra pendente com mesmo código) num `if ($tipo === 'sugestao') { ... }`. No final, retornar incluindo `tipo`:
```php
        return ['ok' => true, 'data' => compact('missaoId', 'produtoId', 'codigo', 'tipo')];
```

- [ ] **Step 4: Rodar e confirmar PASSA**

Run: `php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Validators/SinalizarAusenciaValidatorTddTest.php`
Expected: `RESULTADO: PASS>=4 FAIL=0`.

- [ ] **Step 5: Não-regressão do validator existente**

Run:
```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Validators/SugerirCodigoBarrasValidatorTddTest.php
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Validators/SugerirCodigoBarrasValidatorAlocacaoWmsTddTest.php
```
Expected: ambos continuam PASS.

- [ ] **Step 6: Commit** *(se autorizado)*

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Validators/SugerirCodigoBarrasValidator.php \
        componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Validators/SinalizarAusenciaValidatorTddTest.php
git commit -m "feat(sugestoes-codigo): validator aceita ocorrência sem código (tipo=ausencia)"
```

---

### Task 4: Laboratório `SinalizarSemCodigoLaboratorio` + ações no dispatcher

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/SinalizarSemCodigoLaboratorio.php`
- Modify: `componentes/Missoes/Logistica/sinais/AlocacaoWms/AlocacaoWms.php` (ações `sinalizarSemCodigo`, `cancelarSemCodigo`)
- Test: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/SinalizarSemCodigoLaboratorioTddTest.php`

**Interfaces:**
- Consumes: `SugerirCodigoBarrasValidator::validar` (Task 3), `MissaoCodigoSugestaoSonda::registrarAusencia` (Task 2), `MissaoCodigoSugestaoSonda::cancelarPendente`.
- Produces: `SinalizarSemCodigoLaboratorio::executar(array $data): array` → `['ok'=>bool, 'sugestao'=>object]|['ok'=>false,'erro'=>string]`. Espera `$data['missao_origem_id']`, `$data['produto_id']`.

- [ ] **Step 1: Escrever o teste (falha)**

`SinalizarSemCodigoLaboratorioTddTest.php` (bootstrap padrão; missão `ALOCACAO_WMS` ativa block 22; produto sem código):
```php
// ... bootstrap + Connect dev_gx_goldie + check() ...
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios\SinalizarSemCodigoLaboratorio;

$BLOCK = 22;
$PRODUTO = (int)$pdo->query("SELECT id FROM produtos WHERE block={$BLOCK} AND (codigo_barras IS NULL OR codigo_barras='') LIMIT 1")->fetchColumn();
$pdo->exec("INSERT INTO missao_logistica (tipo_missao_id, status, descricao, modo_atribuicao, block, data_cadastro)
            VALUES (14, 'pendente', '__test_sem_codigo_lab', 'FILA_ABERTA', {$BLOCK}, NOW())");
$ORIGEM = (int)$pdo->query("SELECT id FROM missao_logistica WHERE descricao='__test_sem_codigo_lab' AND block={$BLOCK} ORDER BY id DESC LIMIT 1")->fetchColumn();

try {
    $r = (new SinalizarSemCodigoLaboratorio())->executar(['missao_origem_id'=>$ORIGEM, 'produto_id'=>$PRODUTO]);
    check(($r['ok'] ?? false) === true, 'executar ok');
    check(($r['sugestao']->tipo ?? '') === 'ausencia', 'gravou tipo=ausencia');
    $cnt = (int)$pdo->query("SELECT COUNT(*) FROM missao_codigo_sugestao WHERE missao_origem_id={$ORIGEM} AND status='pendente'")->fetchColumn();
    check($cnt === 1, 'uma pendência criada');

    // faltando produto → erro
    $r = (new SinalizarSemCodigoLaboratorio())->executar(['missao_origem_id'=>$ORIGEM]);
    check(($r['ok'] ?? true) === false, 'sem produto → erro');
} finally {
    $pdo->exec("DELETE FROM missao_codigo_sugestao WHERE missao_origem_id={$ORIGEM}");
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$ORIGEM}");
}
echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
exit($falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar e confirmar FALHA**

Run: `php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/SinalizarSemCodigoLaboratorioTddTest.php`
Expected: FAIL (classe não existe).

- [ ] **Step 3: Implementar o laboratório**

`SinalizarSemCodigoLaboratorio.php` (espelha `SugerirCodigoBarrasLaboratorio`, mas grava ausência):
```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios;

use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Validators\SugerirCodigoBarrasValidator;

/**
 * Registra uma ocorrência "produto sem código de barras" (tipo=ausencia) dentro de
 * uma missão de alocação ativa. Não exige código — o valor é atribuído depois, na
 * missão AJUSTE_DADOS gerada na conclusão. Idempotente por (origem, produto).
 */
class SinalizarSemCodigoLaboratorio
{
    public function executar(array $data): array
    {
        $session    = getSession('galaxiaUserInfo');
        $operadorId = (int)($session->userData->id ?? 0);
        $blockAtual = (int)($session->customerData->id ?? 0);

        if ($operadorId <= 0 || $blockAtual <= 0) {
            return ['ok' => false, 'erro' => 'Sessão inválida'];
        }

        $data['tipo'] = 'ausencia';
        $resultado = (new SugerirCodigoBarrasValidator())->validar($data, $blockAtual);
        if (!$resultado['ok']) return $resultado;

        ['missaoId' => $missaoId, 'produtoId' => $produtoId] = $resultado['data'];

        $sugestao = (new MissaoCodigoSugestaoSonda())
            ->registrarAusencia($missaoId, $produtoId, $operadorId);

        if (!$sugestao) return ['ok' => false, 'erro' => 'Falha ao registrar ocorrência'];

        return ['ok' => true, 'sugestao' => $sugestao];
    }
}
```

- [ ] **Step 4: Ligar as ações no dispatcher `AlocacaoWms.php`**

No `match ($acao)` (linha ~39), adicionar:
```php
                'buscarProduto'     => $this->buscarProduto($data),
                'sinalizarSemCodigo'=> $this->sinalizarSemCodigo($data),
                'cancelarSemCodigo' => $this->cancelarSemCodigo($data),
```
Adicionar os métodos (busca fica no Task 5; aqui as duas ocorrências):
```php
    private function sinalizarSemCodigo(array $data): array
    {
        $missaoId  = (int)($data['missao_id'] ?? 0);
        $produtoId = (int)($data['produto_id'] ?? 0);
        if (!$missaoId || !$produtoId) {
            return ['ok' => false, 'erro' => 'missao_id e produto_id são obrigatórios.'];
        }
        return (new \GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios\SinalizarSemCodigoLaboratorio())
            ->executar(['missao_origem_id' => $missaoId, 'produto_id' => $produtoId]);
    }

    private function cancelarSemCodigo(array $data): array
    {
        $missaoId  = (int)($data['missao_id'] ?? 0);
        $produtoId = (int)($data['produto_id'] ?? 0);
        if (!$missaoId || !$produtoId) {
            return ['ok' => false, 'erro' => 'missao_id e produto_id são obrigatórios.'];
        }
        $ok = (new \GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda())
            ->cancelarPendente($missaoId, $produtoId);
        return ['ok' => $ok, 'cancelada' => $ok];
    }
```

- [ ] **Step 5: Rodar e confirmar PASSA**

Run: `php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/SinalizarSemCodigoLaboratorioTddTest.php`
Expected: `RESULTADO: PASS=4 FAIL=0`.

- [ ] **Step 6: Lint do dispatcher**

Run: `php -l componentes/Missoes/Logistica/sinais/AlocacaoWms/AlocacaoWms.php`
Expected: `No syntax errors detected`.

- [ ] **Step 7: Commit** *(se autorizado)*

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/SinalizarSemCodigoLaboratorio.php \
        componentes/Missoes/Logistica/sinais/AlocacaoWms/AlocacaoWms.php \
        componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/SinalizarSemCodigoLaboratorioTddTest.php
git commit -m "feat(alocacao-wms): ação sinalizar/cancelar 'sem código de barras'"
```

---

### Task 5: Busca manual de produto por descrição

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/BuscarProdutoLaboratorio.php`
- Modify: `componentes/Missoes/Logistica/sinais/AlocacaoWms/AlocacaoWms.php` (método `buscarProduto`; ação já registrada no Task 4)
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/BuscarProdutoLaboratorioTddTest.php`

**Interfaces:**
- Produces: `BuscarProdutoLaboratorio::buscar(string $termo, int $block, int $limite = 20): array` → lista de `['produto_id','descricao','codigo_produto','codigo_barras','tem_codigo_barras']`. Read-only.

- [ ] **Step 1: Escrever o teste (falha)**

`BuscarProdutoLaboratorioTddTest.php` (bootstrap padrão; usa a descrição real do produto 16221 pra montar o termo):
```php
// ... bootstrap + Connect dev_gx_goldie + check() ...
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Laboratorios\BuscarProdutoLaboratorio;

$BLOCK = 22;
$desc = (string)$pdo->query("SELECT descricao FROM produtos WHERE id=16221")->fetchColumn();
$termo = substr(trim($desc), 0, 4); // prefixo da descrição real

$lab = new BuscarProdutoLaboratorio();
$res = $lab->buscar($termo, $BLOCK, 20);
check(is_array($res), 'retorna array');
check(count($res) >= 1, 'acha ao menos 1 produto pela descrição');
$achou = false;
foreach ($res as $r) {
    check(array_key_exists('produto_id', $r) && array_key_exists('tem_codigo_barras', $r), 'item tem produto_id + tem_codigo_barras', );
    if ((int)$r['produto_id'] === 16221) $achou = true;
    break;
}
check($achou || count($res) >= 1, 'produto 16221 aparece (ou ao menos retornou resultados)');

// termo vazio → lista vazia (não varre o catálogo inteiro)
check($lab->buscar('', $BLOCK) === [], 'termo vazio → []');

echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
exit($falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar e confirmar FALHA**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/BuscarProdutoLaboratorioTddTest.php`
Expected: FAIL (classe não existe).

- [ ] **Step 3: Implementar o laboratório**

`BuscarProdutoLaboratorio.php` (leitura via Inteligência `Produto`, escopo do tenant injetado pelo framework; `find()` com ILIKE):
```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Laboratorios;

use GalaxiaConstelacoes\Produtos\Crud\Inteligencias\Produto;

/**
 * Busca de produto por descrição ou código interno — usada na execução da
 * Alocação WMS quando não há código de barras pra bipar. Read-only (não cria
 * nada). Resultado alimenta o mesmo caminho de confirmação que a bipagem.
 */
class BuscarProdutoLaboratorio
{
    public function buscar(string $termo, int $block, int $limite = 20): array
    {
        $termo = trim($termo);
        if ($termo === '' || mb_strlen($termo) < 2) return [];

        $like = '%' . $termo . '%';
        $rows = (new Produto())
            ->find(
                '(descricao ILIKE :t OR codigo_produto ILIKE :t)',
                "t={$like}"
            )
            ->limit((int)$limite)
            ->fetch(true);

        if (!$rows) return [];

        $out = [];
        foreach ($rows as $p) {
            $codigoBarras = $p->codigo_barras ?? null;
            $out[] = [
                'produto_id'        => (int)$p->id,
                'descricao'         => $p->descricao ?? '',
                'codigo_produto'    => $p->codigo_produto ?? '',
                'codigo_barras'     => $codigoBarras,
                'tem_codigo_barras' => !empty($codigoBarras),
            ];
        }
        return $out;
    }
}
```
> Nota de implementação: se `Produto::find()` não suportar `:t` repetido no bind-string do framework, duplicar o parâmetro (`t={$like}&t2={$like}`) e usar `:t`/`:t2`. Validar no Step 4; ajustar o bind conforme o padrão real da Inteligência `Produto`.

Adicionar o método no dispatcher `AlocacaoWms.php`:
```php
    private function buscarProduto(array $data): array
    {
        $termo = trim($data['termo'] ?? $data['q'] ?? '');
        $block = (int)(getSession('galaxiaUserInfo')->customerData->id ?? 0);
        if ($block <= 0) return ['ok' => false, 'erro' => 'Sessão inválida.'];
        $resultados = (new \GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Laboratorios\BuscarProdutoLaboratorio())
            ->buscar($termo, $block);
        return ['ok' => true, 'produtos' => $resultados];
    }
```
> Atenção: já existe um método `private function consultarProduto()` (por código). Manter os dois — `buscarProduto` é por descrição/lista, `consultarProduto` é por código/único.

- [ ] **Step 4: Rodar e confirmar PASSA**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/BuscarProdutoLaboratorioTddTest.php`
Expected: `RESULTADO: PASS>=4 FAIL=0`. Se falhar no bind `:t`, aplicar a nota do Step 3.

- [ ] **Step 5: Lint**

Run: `php -l componentes/Missoes/Logistica/sinais/AlocacaoWms/AlocacaoWms.php && php -l componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/BuscarProdutoLaboratorio.php`
Expected: sem erros.

- [ ] **Step 6: Commit** *(se autorizado)*

```bash
git add componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/BuscarProdutoLaboratorio.php \
        componentes/Missoes/Logistica/sinais/AlocacaoWms/AlocacaoWms.php \
        componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/BuscarProdutoLaboratorioTddTest.php
git commit -m "feat(alocacao-wms): busca manual de produto por descrição"
```

---

### Task 6: Gerador — ramo `ausencia` cria missão executável (itens pendentes)

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorio.php`
- Modify: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/GerarAjusteDeSugestoesLaboratorio.php` (rótulo ALOCACAO_WMS)
- Test: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Testes/Laboratorios/CriarAjusteAusenciaTddTest.php`

**Interfaces:**
- Consumes: pendências com `tipo` (Task 2). `StatusMissao::PENDENTE`, `StatusItem::PENDENTE`.
- Produces: quando as sugestões têm `tipo='ausencia'`, a missão `AJUSTE_DADOS` nasce **PENDENTE** com `configuracoes.campos_solicitados=['codigo_barras']` e **itens PENDENTES** (sem `dados_coletados.codigo_barras`). Ramo `sugestao` inalterado.

- [ ] **Step 1: Escrever o teste (falha)**

`CriarAjusteAusenciaTddTest.php` (bootstrap padrão; cria origem ALOCACAO_WMS; monta `sugestoes` com `tipo='ausencia'`):
```php
// ... bootstrap + Connect dev_gx_goldie + check() ...
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Adapters\AjusteDados\Laboratorios\CriarAjusteDadosDeSugestoesLaboratorio;

$BLOCK = 22; $PRODUTO = 16221;
$pdo->exec("INSERT INTO missao_logistica (tipo_missao_id, status, descricao, modo_atribuicao, block, dia_efetivo, data_cadastro)
            VALUES (14, 'concluida', '__test_ajuste_ausencia_origem', 'FILA_ABERTA', {$BLOCK}, CURRENT_DATE, NOW())");
$ORIGEM = (int)$pdo->query("SELECT id FROM missao_logistica WHERE descricao='__test_ajuste_ausencia_origem' AND block={$BLOCK} ORDER BY id DESC LIMIT 1")->fetchColumn();

try {
    $ajusteId = (new CriarAjusteDadosDeSugestoesLaboratorio())->executar([
        'descricao'     => 'Sugestão - Alocação WMS #' . $ORIGEM,
        'referencia_id' => $ORIGEM,
        'sugestoes'     => [ (object)['id'=>0, 'produto_id'=>$PRODUTO, 'codigo_sugerido'=>null, 'tipo'=>'ausencia', 'operador_sugeriu_id'=>1] ],
    ]);
    check($ajusteId > 0, 'criou missão de ajuste');

    $m = $pdo->query("SELECT status, configuracoes FROM missao_logistica WHERE id={$ajusteId}")->fetch(\PDO::FETCH_ASSOC);
    check($m['status'] === 'pendente', 'missão ausência nasce PENDENTE (executável)');
    check(strpos((string)$m['configuracoes'], 'codigo_barras') !== false, 'campos_solicitados inclui codigo_barras');

    $it = $pdo->query("SELECT status, dados_coletados FROM missao_logistica_itens WHERE missao_logistica_id={$ajusteId} LIMIT 1")->fetch(\PDO::FETCH_ASSOC);
    check($it['status'] === 'pendente', 'item nasce PENDENTE (operador atribui o código)');
    check(empty($it['dados_coletados']) || strpos((string)$it['dados_coletados'], 'codigo_barras') === false || json_decode($it['dados_coletados'], true)['codigo_barras'] === '', 'item não traz código pré-preenchido');
} finally {
    $pdo->exec("DELETE FROM missao_logistica_itens WHERE missao_logistica_id IN (SELECT id FROM missao_logistica WHERE referencia_id={$ORIGEM} AND tipo_missao_id=1)");
    $pdo->exec("DELETE FROM missao_logistica WHERE referencia_id={$ORIGEM} AND tipo_missao_id=1");
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$ORIGEM}");
}
echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
exit($falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar e confirmar FALHA**

Run: `php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Testes/Laboratorios/CriarAjusteAusenciaTddTest.php`
Expected: FAIL (hoje nasce AGUARDANDO_APROVACAO com item CONCLUIDO).

- [ ] **Step 3: Implementar o ramo `ausencia`**

Em `CriarAjusteDadosDeSugestoesLaboratorio::executar`, logo após validar `$sugestoes`, detectar o modo:
```php
        $ehAusencia = false;
        foreach ($sugestoes as $s) {
            if (($s->tipo ?? 'sugestao') === 'ausencia') { $ehAusencia = true; break; }
        }
```
No `make()` da missão (linha ~56), parametrizar status/timestamps por modo:
```php
        $missao->make([
            'tipo_missao_id'   => TipoMissao::AJUSTE_DADOS->id(),
            'descricao'        => $descricao,
            'status'           => $ehAusencia ? StatusMissao::PENDENTE->value : StatusMissao::AGUARDANDO_APROVACAO->value,
            'modo_atribuicao'  => 'FILA_ABERTA',
            'referencia_tipo'  => TipoMissao::AJUSTE_DADOS->value,
            'referencia_id'    => $referenciaId,
            'requer_aprovacao' => 'true',
            'configuracoes'    => json_encode(['campos_solicitados' => ['codigo_barras']]),
            'dia_efetivo'      => $origem->dia_efetivo,
            'iniciado_em'      => $ehAusencia ? null : $agora,
            'finalizado_em'    => $ehAusencia ? null : $agora,
        ]);
```
No loop de itens, ramificar status e `dados_coletados`:
```php
            $item = new MissaoLogisticaItem();
            $item->make([
                'missao_logistica_id' => $ajusteId,
                'produto_id'          => $produtoId,
                'status'              => $ehAusencia ? StatusItem::PENDENTE->value : StatusItem::CONCLUIDO->value,
                'ordem'               => $ordem,
                'quantidade'          => 1,
                'unidade'             => 'UN',
                // ausência: SEM dados_coletados (operador atribui o código na execução)
                'dados_coletados'     => $ehAusencia ? null : json_encode($dadosColetados),
                'dados_adicionais'    => json_encode($dadosAdicionais),
                'iniciado_em'         => $ehAusencia ? null : $sugeridaEm,
                'iniciado_por'        => $ehAusencia ? null : ($operadorId ?: null),
                'concluido_em'        => $ehAusencia ? null : $agora,
                'concluido_por'       => $ehAusencia ? null : ($operadorId ?: null),
                'hash_referencia'     => md5($referenciaId . '|0|0|' . $produtoId . '|1'),
            ]);
```
Garantir o `use` de `StatusItem`/`StatusMissao`/`StatusMissao::PENDENTE` (StatusMissao já importado; StatusItem já importado).

- [ ] **Step 4: Rótulo ALOCACAO_WMS no gerador**

Em `GerarAjusteDeSugestoesLaboratorio::executar`, no `match ($tipoMissaoId)`:
```php
        $tipoLabel = match ($tipoMissaoId) {
            TipoMissao::SEPARACAO->id()    => 'Separação',
            TipoMissao::CARREGAMENTO->id() => 'Carregamento',
            TipoMissao::ALOCACAO_WMS->id() => 'Alocação WMS',
            default                         => 'Missão',
        };
```

- [ ] **Step 5: Rodar e confirmar PASSA**

Run: `php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Testes/Laboratorios/CriarAjusteAusenciaTddTest.php`
Expected: `RESULTADO: PASS>=5 FAIL=0`.

- [ ] **Step 6: Não-regressão do gerador (fluxo sugestao)**

Run: `php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Testes/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorioTddTest.php`
Expected: continua PASS (missão com código nasce AGUARDANDO_APROVACAO + item CONCLUIDO).

- [ ] **Step 7: Commit** *(se autorizado)*

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorio.php \
        componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/GerarAjusteDeSugestoesLaboratorio.php \
        componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Testes/Laboratorios/CriarAjusteAusenciaTddTest.php
git commit -m "feat(sugestoes-codigo): ocorrência sem código gera AJUSTE_DADOS executável (itens pendentes)"
```

---

### Task 7: Hook de conclusão — habilitar `ALOCACAO_WMS`

**Files:**
- Modify: `componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesStatus.php:452-454`
- Modify: `componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesAprovacao.php:515`
- Test: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/GerarAjusteAlocacaoConclusaoTddTest.php`

**Interfaces:**
- Consumes: `GerarAjusteDeSugestoesLaboratorio::executar(int $missaoOrigemId): ?int` (já existe).
- Produces: ao concluir uma missão `ALOCACAO_WMS` com pendência, o ajuste é gerado.

- [ ] **Step 1: Escrever o teste de integração (falha)**

`GerarAjusteAlocacaoConclusaoTddTest.php` — testa o gerador disparado com origem ALOCACAO_WMS + pendência `ausencia` (valida o caminho ponta-a-ponta do gerador; a chamada pelo hook é coberta pela mudança da whitelist):
```php
// ... bootstrap + Connect dev_gx_goldie + check() ...
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios\GerarAjusteDeSugestoesLaboratorio;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda;

$BLOCK = 22; $PRODUTO = 16221;
$pdo->exec("INSERT INTO missao_logistica (tipo_missao_id, status, descricao, modo_atribuicao, block, dia_efetivo, data_cadastro)
            VALUES (14, 'em_andamento', '__test_hook_alocacao', 'FILA_ABERTA', {$BLOCK}, CURRENT_DATE, NOW())");
$ORIGEM = (int)$pdo->query("SELECT id FROM missao_logistica WHERE descricao='__test_hook_alocacao' AND block={$BLOCK} ORDER BY id DESC LIMIT 1")->fetchColumn();
(new MissaoCodigoSugestaoSonda())->registrarAusencia($ORIGEM, $PRODUTO, 1);

try {
    $ajusteId = (new GerarAjusteDeSugestoesLaboratorio())->executar($ORIGEM);
    check($ajusteId !== null && $ajusteId > 0, 'gerou ajuste a partir de origem ALOCACAO_WMS');

    // idempotência: 2ª chamada não duplica
    $again = (new GerarAjusteDeSugestoesLaboratorio())->executar($ORIGEM);
    check($again === null, 'idempotente: 2ª finalização não duplica');

    // pendência consumida
    $pend = (int)$pdo->query("SELECT COUNT(*) FROM missao_codigo_sugestao WHERE missao_origem_id={$ORIGEM} AND status='pendente'")->fetchColumn();
    check($pend === 0, 'ocorrência marcada como consumida');
} finally {
    $pdo->exec("DELETE FROM missao_codigo_sugestao WHERE missao_origem_id={$ORIGEM}");
    $pdo->exec("DELETE FROM missao_logistica_itens WHERE missao_logistica_id IN (SELECT id FROM missao_logistica WHERE referencia_id={$ORIGEM} AND tipo_missao_id=1)");
    $pdo->exec("DELETE FROM missao_logistica WHERE referencia_id={$ORIGEM} AND tipo_missao_id=1");
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$ORIGEM}");
}
echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
exit($falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar e confirmar PASSA já no gerador (sanidade)**

Run: `php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/GerarAjusteAlocacaoConclusaoTddTest.php`
Expected: PASS (o gerador não filtra por tipo de origem — ele já aceita qualquer origem com pendência; este teste valida o caminho `ausencia`). Se falhar, revisar Task 6.

- [ ] **Step 3: Habilitar `ALOCACAO_WMS` na whitelist do hook (MissoesStatus)**

`MissoesStatus.php` linha ~454, trocar a whitelist:
```php
        if (
            $novoStatus === StatusMissao::CONCLUIDA &&
            in_array((int)$missao->tipo_missao_id, [TipoMissao::SEPARACAO->id(), TipoMissao::CARREGAMENTO->id(), TipoMissao::ALOCACAO_WMS->id()], true)
        ) {
```

- [ ] **Step 4: Espelhar em MissoesAprovacao**

`MissoesAprovacao.php` linha ~515, trocar:
```php
        if ($missao && in_array((int)$missao->tipo_missao_id, [TipoMissao::SEPARACAO->id(), TipoMissao::CARREGAMENTO->id(), TipoMissao::ALOCACAO_WMS->id()], true)) {
```

- [ ] **Step 5: Validar o caminho de conclusão da Alocação WMS**

Confirmar **qual** satélite leva a `ALOCACAO_WMS` a CONCLUIDA (sessão livre). Run:
```bash
grep -rn "CONCLUIDA\|concluir\|finalizar" componentes/Missoes/Logistica/sinais/App/Handlers/AlocacaoWmsHandler.php componentes/Missoes/Logistica/sinais/App/Handlers/BaseHandler.php | head -30
```
Expected: identificar se passa por `MissoesStatus::transicionar` e/ou `MissoesAprovacao`. Se a alocação NÃO requer aprovação, o gatilho relevante é o de `MissoesStatus` (Step 3). Documentar o achado no PR/diff. (Ambos já habilitados; este passo é verificação.)

- [ ] **Step 6: Lint**

Run: `php -l componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesStatus.php && php -l componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesAprovacao.php`
Expected: sem erros.

- [ ] **Step 7: Commit** *(se autorizado)*

```bash
git add componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesStatus.php \
        componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesAprovacao.php \
        componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/GerarAjusteAlocacaoConclusaoTddTest.php
git commit -m "feat(missoes): hook de conclusão gera ajuste de código também p/ ALOCACAO_WMS"
```

---

### Task 8: Frontend — busca manual + botão "sem código" na execução

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/App/html/alocacaowms/item.php`
- Modify: `componentes/Missoes/Logistica/sinais/App/html/alocacaowms/item.js`

**Interfaces (consumidas do backend):**
- `POST` ao dispatcher `/missao/Logistica/AlocacaoWms` com `sinalGxData='buscarProduto'`, `termo` → `{ok, produtos:[{produto_id, descricao, codigo_produto, codigo_barras, tem_codigo_barras}]}`.
- `sinalGxData='sinalizarSemCodigo'`, `missao_id`, `produto_id` → `{ok, sugestao}`.
- `sinalGxData='cancelarSemCodigo'`, `missao_id`, `produto_id` → `{ok, cancelada}`.

> Implementação por **genesis-dev-frontend**, seguindo o padrão Metronic + JS pareado já usado em `item.js`/`inventario_endereco.js`. Todos os textos via `show('...')`.

- [ ] **Step 1: UI de busca manual no `item.php`**

Adicionar, próximo ao input de bipagem do produto, um bloco "Buscar por descrição" (colapsável): um `<input id="alocacao-busca-termo">` + `<button id="alocacao-busca-btn">` + container `<ul id="alocacao-busca-resultados">`. Texto via `show('Sem o código? Buscar por descrição')`, `show('Buscar')`, `show('Nenhum produto encontrado')`.

- [ ] **Step 2: JS — disparar a busca e renderizar resultados**

Em `item.js`, ligar o botão/`keyup(Enter)` do termo a um `fetch` (mesmo helper AJAX já usado no arquivo) com `sinalGxData:'buscarProduto', termo`. Renderizar cada resultado como linha clicável mostrando `descricao` + `codigo_produto`; quando `tem_codigo_barras===false`, marcar visualmente ("sem código de barras"). Selecionar uma linha deve setar o `produto_id` ativo e seguir o **mesmo caminho** que uma bipagem bem-sucedida (reusar a função existente que estagia/confirma o produto — não duplicar a lógica de confirmação).

- [ ] **Step 3: Botão "Sinalizar sem código de barras"**

Quando o produto ativo tem `tem_codigo_barras===false` (ou resposta do `consultarProduto`/`buscarProduto` indicar ausência), exibir botão `show('Sinalizar sem código de barras')`. No clique: `fetch` com `sinalGxData:'sinalizarSemCodigo', missao_id, produto_id`; on success, feedback `show('Ocorrência registrada — será ajustada depois')` e oferecer desfazer (`cancelarSemCodigo`). Reusar o sistema de toast/feedback já presente no arquivo.

- [ ] **Step 4: Cache-bust do JS (se aplicável ao padrão da view)**

Se `item.php` referencia `item.js` com `?v=N` manual, bumpar `N`. (Se o pareamento usa `filemtime` via `GalaxiaRoute::visual()`, não precisa — ver memória `galaxia_visual_js_filemtime`.) Confirmar qual é o caso em `item.php`.

- [ ] **Step 5: Lint dos arquivos**

Run: `php -l componentes/Missoes/Logistica/sinais/App/html/alocacaowms/item.php`
Expected: sem erros. (JS: validar carregamento no browser no Step 6.)

- [ ] **Step 6: Smoke no browser (manual — não headless)**

Abrir uma missão `ALOCACAO_WMS` ativa no app (block 22 / dev): (a) buscar um produto por descrição → selecionar → confirmar alocação; (b) com produto sem código, clicar "Sinalizar sem código" → ver feedback; (c) desfazer. Conferir no banco dev: `SELECT * FROM missao_codigo_sugestao WHERE missao_origem_id=<missao> ORDER BY id DESC;`.

- [ ] **Step 7: Commit** *(se autorizado)*

```bash
git add componentes/Missoes/Logistica/sinais/App/html/alocacaowms/item.php \
        componentes/Missoes/Logistica/sinais/App/html/alocacaowms/item.js
git commit -m "feat(alocacao-wms): UI de busca manual + sinalizar produto sem código"
```

---

### Task 9: Integração ponta-a-ponta + docs

**Files:**
- Create: `tests/integration/AlocacaoWms/OcorrenciaSemCodigoFluxoTest.php` (ou diretório de integração equivalente já usado)
- Modify: `componentes/Missoes/Logistica/sinais/AlocacaoWms/README.md` (seção "Ocorrência sem código + busca manual")
- Modify: `PRD/operacao-wms/02-techplan.md` (status da extensão)

**Interfaces:**
- Consumes: tudo das Tasks 1–7.

- [ ] **Step 1: Teste de fluxo (falha antes de tudo estar verde — aqui só consolida)**

`OcorrenciaSemCodigoFluxoTest.php`: cria origem `ALOCACAO_WMS` ativa (block 22) → `SinalizarSemCodigoLaboratorio::executar` (produto sem código) → `GerarAjusteDeSugestoesLaboratorio::executar(origem)` → assertiva: existe 1 missão `AJUSTE_DADOS` PENDENTE com 1 item PENDENTE e `campos_solicitados=['codigo_barras']`; pendência consumida; idempotência. Cleanup completo no `finally`.

- [ ] **Step 2: Rodar e confirmar PASSA**

Run: `php tests/integration/AlocacaoWms/OcorrenciaSemCodigoFluxoTest.php`
Expected: `RESULTADO: PASS=... FAIL=0`.

- [ ] **Step 3: Rodar a regressão completa do sinal**

Run:
```bash
for t in \
  componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Migrations/SuporteAusenciaSchemaTddTest.php \
  componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Sondas/RegistrarAusenciaTddTest.php \
  componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Sondas/MissaoCodigoSugestaoSondaTddTest.php \
  componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Validators/SinalizarAusenciaValidatorTddTest.php \
  componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Validators/SugerirCodigoBarrasValidatorTddTest.php \
  componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Validators/SugerirCodigoBarrasValidatorAlocacaoWmsTddTest.php \
  componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/SinalizarSemCodigoLaboratorioTddTest.php \
  componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/GerarAjusteAlocacaoConclusaoTddTest.php \
  componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Testes/Laboratorios/CriarAjusteAusenciaTddTest.php \
  componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Testes/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorioTddTest.php \
  componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/BuscarProdutoLaboratorioTddTest.php \
  ; do echo "== $t =="; php "$t" || echo "FALHOU: $t"; done
```
Expected: todos PASS.

- [ ] **Step 4: Atualizar README + techplan**

No `AlocacaoWms/README.md`, adicionar seção descrevendo: busca manual (`buscarProduto`), ocorrência sem código (`sinalizarSemCodigo`/`cancelarSemCodigo`, reusa `SugestoesCodigoBarras` com `tipo='ausencia'`), e a geração da `AJUSTE_DADOS` executável na conclusão. No `02-techplan.md`, marcar a extensão como concluída e linkar a spec `10-...` e este plano `11-...`.

- [ ] **Step 5: Commit** *(se autorizado)*

```bash
git add tests/integration/AlocacaoWms/OcorrenciaSemCodigoFluxoTest.php \
        componentes/Missoes/Logistica/sinais/AlocacaoWms/README.md \
        PRD/operacao-wms/02-techplan.md
git commit -m "test(alocacao-wms): fluxo ocorrência sem código + docs"
```

---

## Self-Review (preenchido)

**1. Spec coverage:**
- US1 (busca por descrição) → Task 5 (+ Task 8 UI). ✅
- US2 (ocorrência sem código, idempotente, cancelável, regras) → Tasks 2, 3, 4 (+ Task 8 UI). ✅
- US3 (gerar AJUSTE_DADOS executável na conclusão, idempotente, ramo sugestao intacto) → Tasks 6, 7 (+ Task 9 E2E). ✅
- Modelo de dados (tipo + nullable) → Task 1. ✅
- Riscos da spec: `$required` da Sonda → Task 2 Step 3; caminho de conclusão da alocação → Task 7 Step 5; execução do ajuste atribuir código → **já resolvido** (`AjusteDadosExecucaoStrategy::validarConclusao` exige código pra concluir). ✅

**2. Placeholder scan:** sem TBD/TODO; código concreto em cada step. As únicas notas de "ajustar conforme o padrão real" (bind `:t` da Inteligência `Produto`; cache-bust do JS) têm instrução explícita de verificação no próprio step. ✅

**3. Type consistency:** `registrarAusencia(int,int,int):?object`, `SinalizarSemCodigoLaboratorio::executar(array):array`, `BuscarProdutoLaboratorio::buscar(string,int,int):array`, `tipo∈{sugestao,ausencia}`, `GerarAjusteDeSugestoesLaboratorio::executar(int):?int` — consistentes entre tasks e com o código existente. ✅

## Ordem de execução
1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9. (Front na Task 8 depende das ações 4/5; E2E na 9 depende de 1–7.)
