# Implementação backend — Fase 3 (integracao-wms-desacoplada)

Escopo: 1º tradutor concreto (`alocacao`) ponta a ponta + os dois fixes do
review da Fase 2 (M1 — tenant via linha autenticada do canal; n1 — bloco morto
do receiver vira a tradução real). Passos 16 (parcial, só `alocacao`) do
`02-techplan.md`. `separacao`/`movimentacao`/`inventario` seguem sem tradutor
(rejeitados com motivo — comportamento correto e esperado, igual à Fase 2).

**Atualização pós-review** (`12-review-fase3.md`): corrigido o blocker B1 +
os minors m1/m2 dessa rodada de review. Ver seção dedicada logo abaixo.

## Correções do review da Fase 3 (`12-review-fase3.md`)

### B1 (blocker) — missão de origem WMS nascia exigindo aprovação de gestor

**Causa**: a feature de aprovação de gestor (alheia, mesmo working tree)
removeu de `GestaoMissoesAlocacaoWms.php` a linha que forçava
`requer_aprovacao='0'` para missões `ALOCACAO_WMS`, passando o controle para
o painel (checkbox, default marcado). `AlocacaoOrdemTradutor` não reasseria
essa chave, então a missão de origem WMS caía no default `'1'` de
`GestaoMissoesBase.php` → o movimento nascia `aprovacao_gestor='pendente'` →
o cron só empurra `'aprovado'` → `ReportarOrdemLaboratorio` nunca avançava →
`wms_ordens` ficava presa em `traduzida` para sempre, silenciosamente.

**Decisão tomada** (opção *a* do review): **ordem vinda do WMS NÃO exige
aprovação de gestor** — o WMS é quem ordenou o trabalho (mesma lógica do
techplan §Riscos #4, "confiar na ordem do WMS"), e o report-back automático é
o AC central desta fase; retê-lo atrás de uma aprovação manual que ninguém
sabe que é necessária quebraria essa promessa. Gate de aprovação para ordem
externa (ex.: config por empresa que decida se ordens WMS entram no fluxo de
aprovação) fica como evolução futura — fora de escopo desta fase.

**Fix**: `AlocacaoOrdemTradutor::traduzir()` agora seta explicitamente
`'requer_aprovacao' => '0'` no `$data` da missão (mesmo padrão opt-in dos
outros campos que o tradutor já injeta — `referencia_tipo`/`referencia_id`/
`origem_wms`).

**Testes**:
- `AlocacaoOrdemTradutorTddTest.php` — novo assert de regressão: a missão
  criada tem `requer_aprovacao === 0`. Confirmado que esse assert **falha**
  sem o fix (removi a linha manualmente, rodei o teste — `FAIL=1` no assert
  novo — e restaurei o fix).
- `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php` (GRUPO D3) — o
  teste mascarava o bug: criava o movimento via `WmsMovimentoSonda::registrarMovimento()`
  (que nasce `aprovacao_gestor=APROVADO` por default, pulando
  `ConfirmarAlocacaoLaboratorio`, onde o gate real mora) e forçava `ENVIADO`
  na mão. Ajustado para:
  1. Assertar explicitamente, ANTES de simular o push, que a missão criada
     pelo tradutor tem `requer_aprovacao=0` (a mesma leitura que
     `ConfirmarAlocacaoLaboratorio::executar()` faz — `(bool)(int)$missao->requer_aprovacao`).
  2. Computar o `AprovacaoGestor` do fixture a partir desse mesmo valor
     (`PENDENTE` se `requer_aprovacao` fosse truthy, `APROVADO` caso
     contrário) em vez de aceitar o default hardcoded do método, e assertar
     que o movimento nasceu `aprovacao_gestor='aprovado'`.
  - **Confirmado que o D3 agora pega a regressão**: removi de novo a linha do
    fix, rodei a suíte de integração inteira — os 2 asserts novos falham
    (`FAIL=3` no total, incluindo o bug pré-existente do GRUPO E, contra
    `FAIL=1` normal) e o processo sai com **exit code 1** (antes saía 0,
    porque `$falhas > $bugs` vira verdadeiro) — e restaurei o fix.

### m1 (minor) — retorno de `atualizarStatus('traduzida', …)` não era checado

**Onde**: `ReceberOrdemLaboratorio.php`, caminho de sucesso. Se esse UPDATE
falhasse (raro — hiccup de banco logo após a missão já ter sido criada), o
WMS recebia `aceita` mas `wms_ordens` ficava com `status='recebida'` e
`missao_logistica_id=NULL` — órfã do ciclo de report para sempre (a missão
existe e roda normalmente, só o rastreamento da ordem quebra).

**Fix (barato)**: agora o retorno de `atualizarStatus()` é checado; se
`false`, loga via `error_log` com `ordem_id`/`missao_id` para reconciliação
manual. Não revertemos a missão (ela é válida) nem mudamos a resposta ao WMS
(a criação em si foi um sucesso) — só deixamos rastro para quem for investigar.

### m2 (minor) — `resolverProdutoIds` descartava item não resolvido em silêncio

**Onde**: `AlocacaoOrdemTradutor::resolverProdutoIds()`. Como
`OrdemIdentidadeValidator` já confirmou a identidade a montante, um
`produto_ref` que não resolve aqui indicaria uma corrida rara — mas o `else`
era silencioso, então uma ordem com N itens virando missão com N-1 não
deixava nenhum rastro.

**Fix (barato)**: `error_log` quando um item não resolve, citando o
`produto_ref` — evita depuração às cegas depois. Comportamento funcional
inalterado (o item continua sendo omitido da seleção; sessão livre absorve).

### n1/n2 (nits) — aceitos sem alteração

- **n1** — `AlocacaoOrdemTradutor` lê `Produto`/`MissaoLogistica` via V1
  `Inteligencias` (leitura, não estende) — mesma avaliação do m3 da Fase 2 e
  do próprio n1 deste review: é exatamente o padrão dos Laboratórios irmãos
  de `AlocacaoWms`. Migrar por consistência só se/quando o domínio ganhar uma
  read-Sonda V2 canônica de missão/produto. Não fiz nada agora.
- **n2** — se `GestaoMissoes*` retornar sucesso mas
  `localizarMissaoPorReferencia()` não achar a missão (corrida/replicação —
  praticamente impossível dado que `referencia_id=wms_ordens.id` é único e
  recém-gravado), o tradutor devolve `ok=false` e a missão pode existir órfã
  de `wms_ordens`. Risco desprezível, já documentado no código; não fiz nada
  agora (aceito como está).

## Arquivos criados

- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Tradutores/AlocacaoOrdemTradutor.php`
  — traduz uma ordem WMS `tipo='alocacao'` numa missão `TipoMissao::ALOCACAO_WMS`
  (id 14), reusando `GestaoMissoesAlocacaoWms::processarEnvio` (não reimplementa
  persistência). Resolve `produto_id` a partir de `produto_ref`/`importacao_id`
  de cada item; marca `referencia_tipo='WMS_ORDEM'`, `referencia_id=wms_ordens.id`,
  `configuracoes.origem='wms'` e `requer_aprovacao='0'` (fix B1, ver seção
  dedicada abaixo) na própria criação (não em um PATCH posterior). Loga (m2)
  quando um `produto_ref` validado a montante não resolve na tradução.
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Tradutores/AlocacaoOrdemTradutorTddTest.php`
  — integration test (dev_gx_goldie): 2 itens válidos → missão + itens corretos;
  sem itens → sessão livre; item com produto_ref desconhecido → omitido sem
  quebrar; `requer_aprovacao=0` na missão criada (assert de regressão do B1).
  24 asserts.

## Arquivos alterados

- `componentes/Missoes/Plataforma/sinais/Api/Handlers/Webhooks/InboundHandler.php`
  — `new $targetLab($payload)` → `new $targetLab($payload, $endpoint)` (M1).
  Confirmado que PHP ignora argumentos extras em construtores que não os
  declaram (testado isoladamente) — nenhum outro `target_lab` registrado hoje
  além do próprio `ReceberOrdemLaboratorio` (conferido em `webhook_endpoints`
  do `dev_gx_goldie`), então não há risco de quebra a outros canais.
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Laboratorios/ReceberOrdemLaboratorio.php`
  — reescrito:
  - Construtor `__construct(array $data, ?object $endpoint = null)`. Nova
    `resolverEmpresaId()`: fonte da verdade é `$endpoint->block_empresa_id`
    (fallback `customer_id`); fallback por URL só quando `$endpoint` não é
    informado (retrocompat); se ambos existirem e discordarem → rejeita
    fail-closed e loga a divergência (nunca decide por maioria/preferência).
  - n1: bloco morto (`'Tradutor não implementado nesta versão.'`, inalcançável
    na Fase 2) virou o fluxo real: cria `wms_ordens` (status `recebida`) →
    `TraducaoOrdemFactory::create()` → `$tradutor->traduzir($data, $empresaId,
    $ordemId)` → sucesso: `atualizarStatus($ordemId, 'traduzida', null,
    $missaoId)`; falha (tipo sem tradutor, exceção do tradutor, ou
    `$resultado['ok']=false`): `atualizarStatus($ordemId, 'rejeitada', $motivo)`.
    A ordem **sempre** é criada antes da tradução (nunca fica sem persistir se
    a tradução falhar) e a missão **nunca** nasce "meio pronta" — se o
    tradutor não conseguir finalizar a missão, ele mesmo é responsável por não
    deixar rastro (ver `AlocacaoOrdemTradutor`, que localiza a missão pela
    dupla referencia_tipo/id que ele mesmo gravou — não há um caminho onde a
    missão exista sem essa marca).
  - m1 (barato, aplicado — Fase 2): `criarOrdem()` agora trata a colisão UNIQUE em
    corrida — se `WmsOrdemSonda::criar()` retornar `null`, re-busca por
    `external_ref` e devolve o veredito `ja_processada` da linha vencedora em
    vez de um erro genérico "Falha ao registrar a ordem recebida." (só cai
    nesse erro genérico se a re-busca também não encontrar nada, o que
    indicaria uma falha de banco real, não uma corrida).
  - m1 (barato, aplicado — review Fase 3): retorno de `atualizarStatus(...,
    'traduzida', ...)` agora é checado no caminho de sucesso; se `false`, loga
    para reconciliação manual (ver "Correções do review da Fase 3" acima).
  - Docblock da classe corrigido (não descreve mais o `InboundHandler`
    pré-fix; hoje ele propaga o veredito rico no corpo HTTP, 200/422).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Tradutores/TraducaoOrdemInterface.php`
  — `traduzir(array $ordem, int $empresaId)` → `traduzir(array $ordem, int
  $empresaId, int $ordemId)`. Ver "Desvios do techplan" — nenhuma implementação
  existia antes desta entrega (mapa da Factory vazio na Fase 2), então não há
  breaking change real.
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Tradutores/TraducaoOrdemFactory.php`
  — mapa ganha `'alocacao' => AlocacaoOrdemTradutor::class`; comentários dos
  3 tipos restantes viram TODO explícito para o passo 17.
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Sondas/WmsOrdemSonda.php`
  — m4 (barato, aplicado): `buscarPorMissaoId(int $missaoLogisticaId, ?int
  $empresaId = null)` — quando informado, adiciona `AND block_empresa_id =
  :eid` (defesa em profundidade, alinhada com `buscarPorExternalRef`).
  Assinatura backward-compatible (2º parâmetro opcional).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Laboratorios/ReportarOrdemLaboratorio.php`
  — passa a resolver `empresaId` da sessão sintética corrente e repassá-lo a
  `buscarPorMissaoId()` (consumindo o m4 acima).
- `componentes/Missoes/Logistica/sinais/Designacoes/Laboratorios/GestaoMissoesAlocacaoWms.php`
  — ver "Desvios do techplan" (item 1): 3 pontos de extensão ganharam um
  segundo caminho opt-in, 100% backward-compatible (confirmado pela suíte
  `GestaoMissoesAlocacaoWmsTddTest`, 30/30 verde sem alteração):
  - `getConfiguracoesAdicionais()`: `if (!empty($data['origem_wms'])) { $config['origem'] = 'wms'; }`.
  - `extrairReferenciaId()`: `return $this->inputData['referencia_id'] ?? null;` (era sempre `null`).
  - `getReferenciaTipo()`: `return (string) ($this->inputData['referencia_tipo'] ?? TipoMissao::ALOCACAO_WMS->value);` (era sempre o valor fixo).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Tradutores/TraducaoOrdemFactoryTddTest.php`
  — atualizado: `create(OrdemTipo::ALOCACAO)` não lança mais (devolve
  `AlocacaoOrdemTradutor`); os outros 3 tipos continuam lançando;
  `tiposSuportados() === ['alocacao']`.
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Laboratorios/ReceberOrdemLaboratorioTddTest.php`
  — reescrito para o novo fluxo/assinatura: mantém as seções de rejeição da
  Fase 2 (envelope inválido, canal não resolvido, `ativo_entrada=0`, campo
  obrigatório ausente, identidade desconhecida, tipo fora do enum); adiciona
  1b/1c (M1: divergência endpoint×URL → fail-closed; endpoint sozinho é fonte
  da verdade); troca a antiga seção 6 ("`alocacao` sem tradutor") por 6
  (só `separacao`/`movimentacao`/`inventario` seguem sem tradutor) + 7 nova
  (`alocacao` **com** tradutor → aceita de verdade, missão real criada,
  `wms_ordens.status='traduzida'`); adiciona seção 9 (dedup de uma ordem
  **aceita** não cria uma 2ª missão — regressão crítica de se testar agora que
  existe tradução real).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Sondas/WmsOrdemSondaTddTest.php`
  — 2 asserts novos cobrindo o filtro opcional de `buscarPorMissaoId()` (m4).
- `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php` (review Fase 3,
  B1) — GRUPO D3 ajustado para não mascarar mais o gate de aprovação: assert
  de pré-condição (`requer_aprovacao=0` na missão) + fixture de movimento
  computando `AprovacaoGestor` a partir desse mesmo valor (em vez do default
  hardcoded `APROVADO`) + assert de que o movimento nasceu `aprovado`. Ver
  "Correções do review da Fase 3" acima para a prova de que isso pega a
  regressão.

## Desvios do techplan

### 1. `GestaoMissoesAlocacaoWms.php` precisou de 3 pontos de extensão opt-in (não previsto explicitamente no techplan)

O techplan diz para "imitar" `GestaoMissoesAlocacaoWms`/`GestaoMissoesBase::processarEnvio`
e não reimplementar persistência — mas não antecipa que esse template method,
como está hoje, **não tem nenhum jeito de o chamador**:
  (a) recuperar o `missaoId` no caminho de sucesso (o retorno final de
      `processarEnvio()` vem de `AlocacaoWmsStrategy::getSuccessMessage()`,
      que só devolve uma `Message` de texto — o `$sonda->response['missaoId']`
      que `MissoesLogisticas('criacao', ...)` produz internamente **nunca sai
      do método**; confirmado lendo o código e reproduzido num smoke test
      isolado antes de escrever o tradutor "de verdade");
  (b) marcar `referencia_tipo`/`referencia_id` da missão — `GestaoMissoesAlocacaoWms::getReferenciaTipo()`/
      `extrairReferenciaId()` são hardcoded (`'ALOCACAO_WMS'`/`null`), sem ler
      nada de `$data`.

Tentei dois caminhos que NÃO exigiriam tocar em `GestaoMissoesAlocacaoWms.php`
antes de chegar a esta solução, ambos descartados com evidência:
- **`hash_idempotencia`** (coluna já suportada por `MissoesPersistencia::criacao()`,
  usada por outros domínios — Inventariociclico/Lotes — para o mesmo fim de
  "achar de volta o que acabei de criar"): não funciona aqui porque
  `GestaoMissoesBase::processarEnvio()` monta o `$payload` que vai para
  `MissoesLogisticas('criacao', ...)` com uma lista FIXA de chaves, e
  `hash_idempotencia` não está nela — o valor é silenciosamente descartado
  antes de chegar em `MissoesPersistencia::criacao()`. Confirmado com um
  smoke test isolado (o campo voltou `null` no banco).
- **PATCH pós-criação via `MissaoLogistica` direto** (mesmo padrão já usado por
  `ConfirmarAlocacaoLaboratorio::garantirMissaoEmAndamento`, sem tocar em
  `GestaoMissoesAlocacaoWms.php`): funcionaria para gravar `referencia_tipo`/
  `referencia_id`/`configuracoes.origem`, mas ainda esbarra no problema (a) —
  sem `missaoId`, não há linha pra fazer o PATCH. Precisaria de outro
  mecanismo só para achar o id, e o único candidato que sobra
  (`configuracoes`, que É passada adiante) exigiria de qualquer forma tocar em
  `getConfiguracoesAdicionais()` — ou seja, o toque no arquivo compartilhado
  seria inevitável de qualquer jeito.

Dado isso, a extensão mínima e opt-in (3 métodos, cada um com 1-2 linhas
novas, todas atrás de uma chave que o painel de Designações nunca envia)
ficou mais simples e mais correta do que a alternativa "criar a missão errada
e corrigir depois com cancelamento em caso de falha do PATCH" — elimina de
vez a janela onde a missão existiria sem sua marca de origem. Validado que
**nenhuma** das 30 asserções de `GestaoMissoesAlocacaoWmsTddTest` mudou de
comportamento (suíte roda idêntica, 30/30, sem editar o teste).

### 2. Stub de `$galaxiaRoute` precisou de um 2º método além de `enviarSocket()`

Documentado no docblock de `AlocacaoOrdemTradutor::criarGalaxiaStub()`: além
de `enviarSocket()` (já esperado, só dispara com operadores vinculados — não é
o caso aqui), `GestaoMissoesBase::resolverDiaEfetivo()` chama
`$galaxia->setVersao(...)->setConstelacao(...)->...->dadosG(...)` quando não
há um dia de trabalho pendente **e** um `referencia_id` foi informado — e como
o tradutor agora informa `referencia_id` (ponto 1 acima), esse branch passou a
ser alcançável (confirmado batendo de frente com um `Call to undefined method
class@anonymous::setVersao()` no smoke test, antes de eu perceber e expandir
o stub). A ordem WMS não tem romaneio, então o stub simula "sem romaneio
encontrado" (`['status' => 'no']`, o mesmo shape que `is_error()` já entende) —
o método cai no fallback de sempre (`date('Y-m-d')`), sem tocar banco.

### 3. Assinatura de `TraducaoOrdemInterface::traduzir()` ganhou um 3º parâmetro (`$ordemId`)

A Fase 2 definiu `traduzir(array $ordem, int $empresaId): array`, mas não há
como a missão gravar `referencia_id = wms_ordens.id` sem que o tradutor
receba esse id. Como nenhuma implementação concreta existia antes desta
entrega (mapa da Factory vazio até agora), mudar a assinatura não quebra
nada — e é mais limpo do que embutir esse dado dentro do array `$ordem`
(que representa o envelope externo do WMS; misturar um id interno do
Genesis ali seria confuso e arriscaria colidir com uma chave futura do
contrato). `ReceberOrdemLaboratorio` já cria a linha `wms_ordens` **antes**
de chamar o tradutor exatamente para poder repassar esse id.

### 4. Endereço da ordem é validado, mas não anexado ao item da missão

O techplan diz "o tradutor resolve produto_id/endereço". `AlocacaoOrdemTradutor`
resolve `produto_id` (necessário — é o que `GestaoMissoesAlocacaoWms::buscarItens()`
consome) mas **não** tenta anexar o endereço a cada item: Alocação WMS é
"endereço-primeiro / sessão livre" por design (memória
`project_alocacaowms_endereco_primeiro`) — o vínculo produto×endereço é feito
pelo OPERADOR na execução (`ConfirmarAlocacaoLaboratorio`), nunca na criação da
missão; não existe hoje um campo de item para guardar um endereço sugerido, e
criar um seria alterar o modelo de dados do domínio (fora do escopo desta
entrega). A existência do endereço já foi confirmada por
`OrdemIdentidadeValidator` antes de a ordem chegar ao tradutor (AC5.1-5.4) —
a validação acontece, só não há hoje onde "guardar" esse dado na missão.

### 5. `configuracoes.gatilho`/`gerado_automaticamente` ficam mislabeled para missões WMS (cosmético, sem impacto funcional)

Para contornar a exigência de operador de `GestaoMissoesBase::processarEnvio()`
(`(!$autoGen && empty($operadores))` bloqueia missões sem operador, a menos
que `auto_gen=true`), o tradutor passa `auto_gen: true` — o mecanismo já
existe no framework especificamente para "ignorar validação de operador e
sinalizar geração automática" (usado hoje só pelo encadeamento
Separação→Carregamento). Isso faz `GestaoMissoesBase` gravar
`configuracoes.gatilho = 'finalizacao_separacao'` (hardcoded, não
contextual) em qualquer missão criada com `auto_gen=true`, inclusive a nossa.
Conferido que **nenhum** consumidor no repo LÊ essa chave hoje (grep não
encontrou nenhum `->gatilho`/`['gatilho']` fora do próprio ponto de escrita)
— ou seja, é dado morto/só-escrita, sem impacto funcional. Não toquei em
`GestaoMissoesBase.php` para corrigir isso porque afetaria o comportamento
real do encadeamento Separação→Carregamento (fora do escopo desta feature) só
para tornar uma string interna mais precisa.

## Comandos de verificação executados

`php -l` em todo PHP novo/alterado desta fase → **OK** em todos:
- `Tradutores/AlocacaoOrdemTradutor.php` (novo)
- `Tradutores/TraducaoOrdemFactory.php`
- `Tradutores/TraducaoOrdemInterface.php`
- `Laboratorios/ReceberOrdemLaboratorio.php`
- `Laboratorios/ReportarOrdemLaboratorio.php`
- `Sondas/WmsOrdemSonda.php`
- `Api/Handlers/Webhooks/InboundHandler.php`
- `Designacoes/Laboratorios/GestaoMissoesAlocacaoWms.php`
- `Testes/Tradutores/AlocacaoOrdemTradutorTddTest.php` (novo)
- `Testes/Tradutores/TraducaoOrdemFactoryTddTest.php`
- `Testes/Laboratorios/ReceberOrdemLaboratorioTddTest.php`
- `Testes/Sondas/WmsOrdemSondaTddTest.php`
- `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php`

### Suítes executadas contra `dev_gx_goldie`

**Suíte IntegracaoWms** (12 arquivos):
- **Antes da entrega original** (baseline Fase 2, 11 arquivos): `PASS=281 FAIL=0`.
- **Depois da entrega original** (12 arquivos, incl. `AlocacaoOrdemTradutorTddTest.php` novo): `PASS=328 FAIL=0`.
- **Depois das correções do review** (B1 + m1 + m2): `PASS=337 FAIL=0`. O
  incremento de +9 vem de `AlocacaoOrdemTradutorTddTest` (23→24, +1 — assert
  de `requer_aprovacao=0`) e `ReceberOrdemLaboratorioTddTest` (59→67 — seção
  10 de cobertura de corrida do m1 da Fase 2, já presente no working tree).
- Nenhuma regressão em nenhuma das três rodadas.

**Suíte AlocacaoWms** (27 arquivos, domínio irmão só *lido* por esta feature —
nenhum arquivo dele foi alterado; rodada porque o tradutor consome
`OperacaoTipo`/`WmsLocalizacaoSonda`/`Produto` de lá):
- **Antes/depois da entrega original e das correções do review**: `PASS=348 FAIL=12`
  — **números idênticos** nas 3 rodadas, incluindo os mesmos arquivos
  falhando pelos mesmos motivos.
- As 12 falhas + 1 fatal (`HookEstornoTddTest.php`, que não imprime linha de
  resultado) são **pré-existentes** (nada relacionado a esta entrega):
  `CronEstornoSmokeTddTest.php` (2 fail) e `HookEstornoTddTest.php` (fatal)
  falham com `RuntimeException: Provedor WMS não registrado: 'galpao'` — o
  `WmsProviderRegistry` não é bootstrapado quando esses scripts rodam
  isolados fora do `Api\Bootstrap` (gap de infra da Fase 1, não desta feature).
  `ConfirmarAlocacaoValidatorTddTest.php` (12 fail) tem causa não investigada
  a fundo (fora do escopo — não é código tocado por mim; provavelmente
  relacionado ao trabalho não commitado de "aprovação de gestor" já em
  andamento no branch, visível no `git status` antes de eu começar).

**`GestaoMissoesAlocacaoWmsTddTest.php`** (arquivo diretamente alterado —
`GestaoMissoesAlocacaoWms.php`, ver "Desvios" item 1; não precisou de
alteração para o fix de B1, já que B1 foi resolvido inteiramente dentro de
`AlocacaoOrdemTradutor.php`):
- **Antes/depois** (entrega original e correções do review): `PASS=30 FAIL=0`
  — idêntico nas 3 rodadas, sem editar o teste (confirma que os 3 pontos de
  extensão de Fase 3 são 100% opt-in/backward-compatible).

**`tests/integration/IntegracaoWms/ReceberOrdemApiTest.php`** (integration,
ajustado pelo fix do B1 — GRUPO D3):
- `PASS=56 FAIL=1` (o único FAIL é o GRUPO E, bug pré-existente e documentado
  via `expectFixed()`, fora do escopo desta feature — ver `12-review-fase3.md`
  Observações). `exit code 0` (suíte considerada verde: `$falhas <= $bugs`).
- **Confirmado que o D3 pega a regressão do B1**: removendo a linha
  `'requer_aprovacao' => '0'` de `AlocacaoOrdemTradutor.php` e rodando de
  novo, a suíte cai para `PASS=54 FAIL=3` com **exit code 1** — os 2 asserts
  novos do D3 (`requer_aprovacao=0` pré-condição + `aprovacao_gestor='aprovado'`
  no movimento) falham antes de qualquer simulação de push. Fix restaurado
  logo em seguida e reconfirmado verde.

### Smoke manual (fora da suíte formal, documentando o processo de descoberta)

Usei scripts descartáveis fora do repo (`/tmp/.../scratchpad/`) para validar
hipóteses ANTES de escrever o tradutor definitivo — é assim que descobri os
itens 1/2 de "Desvios do techplan" (hash_idempotencia morto, branch
`resolverDiaEfetivo` alcançável). Nenhum desses scripts faz parte da entrega.

## Como testar

```bash
# Unit (sem banco): OrdemTipo/TraducaoOrdemFactory
php componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Tradutores/TraducaoOrdemFactoryTddTest.php

# Integration (dev_gx_goldie): tradutor isolado
php componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Tradutores/AlocacaoOrdemTradutorTddTest.php

# Integration (dev_gx_goldie): receiver ponta-a-ponta (M1 + n1 + dedup)
php componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Laboratorios/ReceberOrdemLaboratorioTddTest.php

# Integration (dev_gx_goldie): ponta-a-ponta via HTTP real (InboundHandler),
# incluindo D3 — guarda de regressão do B1 (requer_aprovacao=0)
php tests/integration/IntegracaoWms/ReceberOrdemApiTest.php

# Regressão do domínio consumido (leitura apenas — nenhum arquivo alterado)
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsLocalizacaoSondaTddTest.php

# Regressão do arquivo compartilhado alterado (Desvios #1)
php componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php
```

Smoke via `curl` (ambiente com o canal `wms-7` seedado, `dev_gx_goldie`):
```bash
BODY='{"version":"v1","external_ref":"smoke-001","tipo":"alocacao","itens":[]}'
SECRET='dev-secret-troque-antes-de-usar-de-verdade'
SIG=$(php -r "echo hash_hmac('sha256', '$BODY', '$SECRET');")
curl -X POST https://dev-genesis.goldieapp.com.br/api/webhooks/wms-7 \
  -H "Content-Type: application/json" -H "X-Signature: sha256=$SIG" -d "$BODY"
# Esperado: HTTP 200 {"received":true,...,"ok":true,"ordem_id":N,"status":"aceita"}
# Conferir: SELECT * FROM wms_ordens WHERE external_ref='smoke-001'; -- status='traduzida'
#           SELECT * FROM missao_logistica WHERE referencia_tipo='WMS_ORDEM' AND referencia_id=N;
```

## Próximos passos para o frontend

Nenhum novo endpoint de UI foi criado nesta fase — como na Fase 2, o receiver
é `/api/webhooks/wms-{empresaId}` (superfície externa, HMAC, não
`/api/v1`, sem consumo direto pelo frontend Genesis).

O que muda de fato a partir de agora: quando uma ordem WMS `tipo='alocacao'`
é aceita, uma missão real de `TipoMissao::ALOCACAO_WMS` aparece no app do
operador **exatamente como qualquer missão nativa de Alocação WMS** — mesma
tela, mesmo fluxo de bipagem/confirmação. A única forma de distinguir
programaticamente uma missão de origem WMS é:
- `missao_logistica.referencia_tipo = 'WMS_ORDEM'` e `referencia_id = <wms_ordens.id>`;
- `missao_logistica.configuracoes` (JSON) tem a chave `origem: "wms"`.

Não há hoje nenhuma indicação visual na tela (nem foi pedido pelo techplan) —
se o produto quiser uma UI de observabilidade (ex.: badge "via WMS" no painel
de Designações, ou uma tela dedicada de acompanhamento de `wms_ordens`), os
dados já existem e são consultáveis (`wms_ordens.status/motivo/missao_logistica_id/recebida_em/reportada_em`),
mas não há Handler de `/api/v1` para isso ainda — não estava no escopo desta
fase.
