# Review — integracao-wms-desacoplada (Fase 3: tradutor `alocacao` real + fixes M1/n1/m1/m4 da Fase 2)

**Base**: `dev_team` @ `661db59f` (working tree não commitado — `project_git_workflow`: sem merge p/ main; diff obtido via `git diff HEAD -- <path>` + untracked)
**HEAD**: `661db59fc32560e912a9b613f6276768f886e364`
**Arquivos tocados (em escopo)**: 2 novos (produção+teste) + 8 alterados de produção/infra + 3 de teste = 13
**Linhas (em escopo)**: novos ~435 (tradutor 212 + teste 223); alterados +740 / -202 (dos quais `GestaoMissoesAlocacaoWms.php` conta 90 linhas mas só ~10 são da Fase 3 — o resto é feature alheia, ver Observações)
**`php -l`**: limpo nos 13 arquivos (sweep re-executado aqui — 0 erros)

## Sumário

Entrega técnica competente e, na maior parte, fiel ao techplan (passo 16, `alocacao`). Os fixes da Fase 2
estão todos aplicados e corretos: **M1** (endpoint autenticado vira fonte-da-verdade do tenant, URL só
fallback, divergência endpoint×URL é fail-closed), **n1** (bloco morto virou tradução real com a ordem
persistida ANTES da tradução), **m1** (colisão UNIQUE em corrida → veredito idempotente) e **m4**
(`buscarPorMissaoId` com filtro opcional de tenant). O `AlocacaoOrdemTradutor` reusa o template method
via os 3 pontos de extensão opt-in — mecanismo são, sem estado estático, backward-compatible por inspeção.

**Porém há 1 blocker que anula a promessa ponta-a-ponta da fase.** A feature de aprovação de gestor (do
working tree, alheia) removeu do arquivo compartilhado `GestaoMissoesAlocacaoWms.php` a linha que forçava
`requer_aprovacao='0'` nas missões de Alocação WMS. O tradutor **não** define `requer_aprovacao` no seu
`$data`, então a missão de origem WMS agora nasce com `requer_aprovacao=1` (default de `GestaoMissoesBase`).
Consequência: o movimento do operador nasce `aprovacao_gestor='pendente'`, o cron só empurra `'aprovado'`,
e a ordem fica **presa em `traduzida` para sempre** até um gestor aprovar manualmente — quebrando
silenciosamente o report-back que é o coração da Fase 3. Não está documentado, e os testes (D3 "ciclo real"
+ TddTest do tradutor) **mascaram** o problema. **Recomendação: voltar pro dev** — o fix é trivial (uma
chave no `$data` do tradutor) ou, se aprovação for decisão de produto, precisa ser documentada e ter o
teste D3 corrigido para não dar falsa confiança.

## Issues

### 🔴 Blocker (1 issue)

#### B1 — Missão de origem WMS nasce exigindo aprovação de gestor → report-back retido silenciosamente
**Onde**: `Tradutores/AlocacaoOrdemTradutor.php:67-77` (o `$data` montado **sem** `requer_aprovacao`) cruzando com `Designacoes/Laboratorios/GestaoMissoesAlocacaoWms.php` (linha `$data['requer_aprovacao']='0'` **removida** pela feature alheia; hoje só o docblock:15 menciona) → cai no default de `GestaoMissoesBase.php:71` (`requer_aprovacao ?? '1'` ⇒ **1**). Efeito em cadeia: `AlocacaoWms/Laboratorios/ConfirmarAlocacaoLaboratorio.php:83-84` (missão com `requer_aprovacao` truthy ⇒ movimento `aprovacao_gestor=PENDENTE`) e `crons/wms_alocacao_integracao.php:108` (só empurra `aprovacao_gestor='aprovado'`).
**Problema**: até o HEAD, `GestaoMissoesAlocacaoWms::processarEnvio` forçava `requer_aprovacao='0'` ("Alocação WMS vai direto ao operador — sem gate de aprovação"). A feature de aprovação (working tree) tirou essa linha e passou o controle "para o painel (checkbox marcado por padrão)". O tradutor da Fase 3, escrito sobre a premissa antiga, **não reasserta** o `'0'` — e, como não há painel/checkbox no fluxo de webhook, a chave simplesmente falta ⇒ default `1`. A partir daí: o operador executa a alocação → o movimento nasce `pendente` → o cron **não** empurra → `ReportarOrdemLaboratorio` nunca avança `wms_ordens.status` → a ordem morre em `traduzida`. O loop inteiro (WMS → missão → execução → push → `reportada`), que a Fase 3 se propõe a "provar ponta-a-ponta", fica travado atrás de uma aprovação manual que ninguém sabe que é necessária.
**Por que é blocker**:
1. **Anula o AC central da fase** (report-back automático) no fluxo real do operador, de forma silenciosa (nenhum erro, nenhum log — a ordem só "não avança").
2. **Não documentado**: `10-backend-fase3.md` não menciona `requer_aprovacao` em nenhum lugar; o dev não avaliou a interação com a feature compartilhada apesar de a mudança estar no mesmo working tree.
3. **Os testes dão falsa confiança**: `AlocacaoOrdemTradutorTddTest.php:148-158` valida `tipo_missao_id`/`referencia_*`/`configuracoes.origem` mas **nunca** asserta `requer_aprovacao`; e o "ciclo de report REAL" (`tests/integration/IntegracaoWms/ReceberOrdemApiTest.php:454-459`, GRUPO D3) cria o movimento via `WmsMovimentoSonda::registrarMovimento(...)` — que **por padrão** já grava `aprovacao_gestor=APROVADO` (`AlocacaoWms/Sondas/WmsMovimentoSonda.php:125`) — e força `ENVIADO` na mão, **pulando** `ConfirmarAlocacaoLaboratorio` (onde o gate mora) e o filtro do cron. Ou seja: o único teste que "prova o report" nunca passa pelo gate.
**Como corrigir** (uma das duas):
- **(a) preservar a automação** (alinhado ao techplan §risco 4 "confiar na ordem do WMS"): o tradutor reasserta o antigo comportamento no próprio `$data` — o mesmo padrão opt-in que ele já usa:
  ```php
  $data = [
      'itens_selecionados' => $produtoIds,
      'operadores'         => [],
      'auto_gen'           => true,
      'requer_aprovacao'   => '0',   // ordem WMS vai direto ao operador (sem gate manual)
      'operacao_tipo'      => OperacaoTipo::ALOCACAO->value,
      ...
  ];
  ```
  e adicionar um assert de regressão (`requer_aprovacao=0` na missão criada) no `AlocacaoOrdemTradutorTddTest`.
- **(b) se aprovação de ordem WMS for decisão de produto**: documentar em `10-backend-fase3.md` como desvio explícito **e** corrigir o GRUPO D3 para exercitar o caminho real (`ConfirmarAlocacaoLaboratorio` → `pendente` → aprovar → cron → `reportada`), senão o teste continua "verde" sobre um loop que, em produção, não fecha.

### 🟡 Major (0 issues)

Nenhum.

### 🟢 Minor (2 issues)

#### m1 — `atualizarStatus('traduzida', …)` com retorno ignorado: falha pós-missão deixa a ordem órfã do ciclo
**Onde**: `Laboratorios/ReceberOrdemLaboratorio.php:160-163`
**Problema (ponto 3 da tarefa — 2ª pergunta)**: no caminho feliz, a missão já foi criada pelo tradutor e então `$sonda->atualizarStatus($ordemId, 'traduzida', null, $missaoId)` grava status + `missao_logistica_id`. O **retorno não é checado** e a linha seguinte devolve `veredito(true, …, 'aceita', …)` incondicionalmente. Se esse `UPDATE` falhar (raro — DB hiccup logo após um create bem-sucedido), o WMS recebe `aceita`, mas a `wms_ordens` fica em `status='recebida'` com `missao_logistica_id=NULL`. Num reenvio, o dedup acha a linha `recebida` (ok=true, pois status≠rejeitada) → `ja_processada`, sem 2ª missão (bom, sem duplicata). Mas a ordem nunca recebe o vínculo de missão, então `ReportarOrdemLaboratorio::buscarPorMissaoId` nunca a encontra ("marcada WMS_ORDEM mas sem wms_ordens vinculada") e ela **fica permanentemente fora do report loop**, com a missão executando normalmente. A missão existe e é válida — só o rastreamento da ordem quebra.
**Como corrigir**: checar o retorno de `atualizarStatus` no caminho de sucesso; se `false`, logar (`error_log`) com `ordem_id`/`missao_id` para reconciliação, e considerar responder algo diferente de `aceita` limpo (ou ao menos deixar rastro). Custo baixo; fecha a única janela de inconsistência silenciosa do pipeline `n1`.

#### m2 — `resolverProdutoIds` descarta itens sem log quando `produto_ref` não resolve
**Onde**: `Tradutores/AlocacaoOrdemTradutor.php:108-132` (`if ($produto && !empty($produto->id))` — o `else` é silencioso)
**Problema**: `OrdemIdentidadeValidator` já confirmou a identidade a montante, mas o tradutor **re-resolve** por `importacao_id` e, se algum item não casar aqui, é omitido em silêncio. Para `alocacao` isso é **aceitável por design** (sessão livre / `permiteSelecaoVazia()` — o operador vincula itens na execução), e o dev documentou (desvio #4). O incômodo é de **observabilidade**: uma ordem que chega com N itens e vira missão com N-1 não deixa nenhum rastro — a discrepância entre validador e tradutor (ou uma corrida) some sem log.
**Como corrigir**: `error_log` (ou anexar ao `wms_ordens.motivo`/payload) quando um `produto_ref` validado não resolve na tradução — barato e evita depuração às cegas depois. Não bloqueia porque o resultado (missão criada, ordem `traduzida`) é legítimo para sessão livre.

### ⚪ Nit (2 issues)

- **n1 — Tradutor lê domínios via V1 `Inteligencias`.** `Tradutores/AlocacaoOrdemTradutor.php:5-6` (`Produto`, `MissaoLogistica`) e `:125`/`:143-147`. É leitura (não estende Inteligencia), e **exatamente** o que os Laboratórios irmãos de `AlocacaoWms` já fazem — mesma avaliação do m3 da Fase 2, aceito. Migrar por consistência se/quando o domínio ganhar read-Sonda V2 canônica de missão/produto. Baixa prioridade.
- **n2 — `localizarMissaoPorReferencia` → `null` num sucesso vira "missão órfã".** `AlocacaoOrdemTradutor.php:90-93` + `:141-149`. Se `GestaoMissoes*` retornar sucesso mas a busca pela dupla `(referencia_tipo, referencia_id)` não achar (corrida/replicação — praticamente impossível dado que o `referencia_id=wms_ordens.id` é único e recém-gravado), o tradutor devolve `ok=false` **e a missão pode existir** → ordem `rejeitada` + missão sem par. Documentado pelo dev; risco desprezível. Só registro para o handoff.

## Verificação dos pontos críticos pedidos

1. **`GestaoMissoesAlocacaoWms` compartilhado**:
   - **(a) zero mudança quando os hooks não são usados** — ✅ **para os 3 pontos de extensão da Fase 3**: `getConfiguracoesAdicionais` (`!empty($data['origem_wms'])`), `extrairReferenciaId` (`$this->inputData['referencia_id'] ?? null`) e `getReferenciaTipo` (`?? TipoMissao::ALOCACAO_WMS->value`) são todos `?? default`/`!empty`-guarded; o painel de Designações nunca envia essas chaves. Confirmado por inspeção.
   - **(b) mecanismo são** — ✅ sem estado estático (leem `$this->inputData` de instância; nova instância por request); o template method de `GestaoMissoesBase` não é quebrado (os hooks são os pontos de extensão que a base define). **Ressalva**: o arquivo também carrega a **remoção do `requer_aprovacao='0'`** (feature alheia) que **muda** o comportamento — é o B1.
   - **(c) `GestaoMissoesAlocacaoWmsTddTest` 30/30** — cobre a não-regressão dos cenários que já testava (verde, idêntico, per docs), mas **não asserta `requer_aprovacao`**, então **não pega o B1**. A cobertura do caminho nativo é boa para o que existia; insuficiente para a nova interação.
2. **M1 (InboundHandler passa `$endpoint`)** — ✅ backward-compat confirmado: `InboundHandler.php:70` faz `new $targetLab($payload, $endpoint)`; PHP ignora argumentos posicionais extras em construtores que não os declaram (só falta-de-args gera `ArgumentCountError`), e hoje só há **um** `target_lab` registrado. `resolverEmpresaId()` usa o endpoint como fonte-da-verdade e **rejeita fail-closed** quando endpoint×URL divergem (`ReceberOrdemLaboratorio.php:222-247`). Fix correto e completo.
3. **n1 (atomicidade criar→traduzir→missão→traduzida)** — ✅ ordem criada **antes** da tradução (id vira `referencia_id`); falha na tradução (tipo sem tradutor / exceção / `ok=false`) → `atualizarStatus('rejeitada', motivo)`, **sem missão órfã** (o tradutor localiza a missão pela dupla que ele mesmo grava, único caminho de sucesso). Reenvio pós-falha → dedup, sem 2ª missão. **Caveat**: `atualizarStatus` falhando **depois** da missão criada = m1 (ordem presa em `recebida`, fora do report loop).
4. **Interação com a feature de aprovação** — ❌ **é o B1**. A missão WMS nasce `requer_aprovacao=1` ⇒ movimentos `aprovacao_gestor='pendente'` ⇒ push/report-back retido até aprovação manual. **Não intencional/documentado** (o tradutor nem seta a chave; o efeito é emergente da mudança alheia no arquivo compartilhado) e **mascarado pelos testes**. Precisa de decisão + fix explícito.
5. **Assinatura `TraducaoOrdemInterface::traduzir($ordem, $empresaId, $ordemId)`** — ✅ coerente: docblock atualizado (`@param int $ordemId`), `AlocacaoOrdemTradutor::traduzir` implementa a nova assinatura, `TraducaoOrdemFactory` devolve a instância, `ReceberOrdemLaboratorio:146` chama com os 3 args. Nenhuma implementação concreta existia na Fase 2, então não há breaking change — decisão limpa (id interno fora do envelope externo `$ordem`).
6. **Bug do `WebhookEndpointSonda::findByChannel()` (dependência de sessão)** — **fora do escopo deste diff** (arquivo de `Api/Sondas`, Plataforma/Fase 1). Registrado aqui em Observações apenas; não é issue da Fase 3.

## Pontos fortes

- **Fix do M1 exemplar**: fonte-da-verdade única (endpoint autenticado), URL rebaixada a fallback retrocompat explícito, e divergência tratada como **fail-closed** com log — resolve exatamente o que o review da Fase 2 apontou, sem sobre-engenharia.
- **Ordem persistida antes da tradução (n1)**: garante o `referencia_id` para a missão e que **toda** ordem deixa rastro em `wms_ordens` mesmo quando a tradução falha — nunca uma missão "meio pronta". Pipeline de rejeição conservador e observável (motivo sempre gravado).
- **`criarOrdem` idempotente em corrida (m1)**: colisão UNIQUE → re-busca e devolve o veredito da linha **vencedora** (preservando ok/motivo originais) em vez de erro genérico. Correto para AC6.3.
- **Reuso do template method sem reimplementar persistência**: o tradutor monta `$data` e delega a `GestaoMissoesAlocacaoWms::processarEnvio` via os pontos de extensão que a base já define — fiel ao molde do techplan. O stub de `$galaxiaRoute` (2 métodos, sem I/O) é uma solução mínima e bem justificada para o contexto sem HTTP.
- **`TraducaoOrdemFactory` continua Factory+enum de mapa fechado** (agora com 1 entrada), com os 3 tipos restantes como TODO explícito — mantém o padrão V2 certo (não virou Registry de string) e a regra "tipo sem tradutor → rejeita com motivo".
- **Isolamento do escopo**: apesar de o working tree carregar 2 features alheias no mesmo arquivo compartilhado, os pedaços da Fase 3 são cirúrgicos e identificáveis por comentário.

## Observações (fora do escopo do diff)

- **Bug pré-existente `WebhookEndpointSonda::findByChannel()`** (`Api/Sondas`, Plataforma/Fase 1): a busca do canal roda com `customerControl/companyControl=true`, dependendo da **sessão ambiente** do processo, não do HMAC — um webhook externo real não carrega sessão Genesis. O Tester reproduziu (`11-testes-fase3.md` §Bugs, GRUPO E) `404 unknown_channel` com HMAC válido quando a sessão não aponta para o tenant do canal. **Não é regressão nem issue desta fase**, mas é potencialmente **bloqueante para produção multi-tenant real** e merece o smoke via `curl` de verdade contra o servidor antes do 1º WMS ir ao ar. Reportar ao dono de `Api/Sondas`.
- **Baseline `AlocacaoWms` (12 fail + 1 fatal)**: confirmada **pré-existente e alheia** (bootstrap do `WmsProviderRegistry` ausente fora do `Api\Bootstrap` + fixture dinâmica de `ConfirmarAlocacaoValidatorTddTest` poluída pela feature multi-tipo). Pin nominal em `11-testes-fase3.md` bate. Não reportada como regressão desta fase.
- **Arquivo compartilhado com 2 features alheias**: `GestaoMissoesAlocacaoWms.php` mistura, no mesmo working tree, os 3 pontos opt-in da Fase 3 + a remoção de `requer_aprovacao='0'` (aprovação de gestor) + `INVENTARIO_ENDERECO`/`resolverAlvosEndereco` (operação multi-tipo). Entrelaçamento inevitável dado o branch único, mas dificulta o `git blame`/revert por feature — só registro.

## Recomendação

⚠️ **Voltar pro dev.** 1 blocker (B1) que quebra silenciosamente o report-back — o AC central da Fase 3 — no fluxo real do operador, sem documentação e com os testes mascarando o defeito. O fix é pequeno (reasserção de `requer_aprovacao='0'` no `$data` do tradutor + assert de regressão) **ou** uma decisão de produto documentada com o teste D3 corrigido para exercitar o gate de verdade. Os fixes M1/n1/m1/m4 da Fase 2 estão corretos e podem ser mantidos. Endereçar junto os minors m1 (retorno de `atualizarStatus` no sucesso) e m2 (log do item descartado) antes do 1º WMS real. Arquitetura fiel às referências V2 — o bloqueio é de correção funcional/interação, não de padrão.

---

## Re-review (Fase 3 — pós-correções)

**HEAD**: `661db59f` (working tree) · **Escopo re-verificado**: fixes de B1, m1, m2 + regressões introduzidas
**`php -l`**: limpo nos 4 arquivos tocados nesta rodada (tradutor, receiver, 2 testes)
**Diff conferido**: `git diff --stat HEAD` dos 11 alterados — só 2 mudaram desde a 1ª passada (`ReceberOrdemLaboratorio.php` 204→211, m1; `ReceberOrdemApiTest.php` 245→269, D3). `GestaoMissoesAlocacaoWms.php` **não foi tocado** pelo fix (correto: B1 resolvido no tradutor, não no arquivo compartilhado). Nenhuma mudança inesperada fora do relatado.

### B1 — ✅ RESOLVIDO (verificado no código, não no relato)
- **Fix no lugar certo**: `Tradutores/AlocacaoOrdemTradutor.php:84` seta `'requer_aprovacao' => '0'` no `$data` da missão, com comentário explicando o porquê (o WMS é quem ordenou; o report-back depende do movimento nascer aprovado). É o padrão opt-in que o tradutor já usava — **não** mexeu no arquivo compartilhado nem no gate alheio (`ConfirmarAlocacaoLaboratorio`/cron intactos). Mecanismo consumido corretamente: `requer_aprovacao=0` → `ConfirmarAlocacaoLaboratorio:83` `(bool)(int)0=false` → `AprovacaoGestor::APROVADO` → cron empurra → `reportada`.
- **Decisão documentada**: `10-backend-fase3.md` §"Correções do review da Fase 3" > B1 registra a decisão (ordem do WMS não exige aprovação de gestor; gate por empresa fica como config futura fora de escopo). Desvio devidamente justificado.
- **Guarda de regressão REAL (dois lugares, ambos lêem a missão do banco)**:
  - `AlocacaoOrdemTradutorTddTest.php:159` — `((int)($missao2['requer_aprovacao'] ?? 1)) === 0`, sobre `$missao2` vindo de `SELECT * FROM missao_logistica`.
  - `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php:464-482` (GRUPO D3 reescrito) — asserta `requer_aprovacao=0` na missão real (`$missaoD` de `SELECT`, linha 427) **antes** de simular o push; deriva `AprovacaoGestor` do valor real (`:468-470`, espelhando `ConfirmarAlocacaoLaboratorio::executar()`) e passa esse valor a `registrarMovimento(...)` (`:474-477`, 13º arg) em vez do default `APROVADO`; então asserta o movimento `aprovacao_gestor='aprovado'` (`:479-482`). Reintroduzir o default `'1'` faz **dois** checks independentes falharem (`:465` e `:480`).
  - **Verificado que o guard NÃO é falso-positivo**: `missao_logistica.requer_aprovacao` é `BOOLEAN` (`MissaoLogistica/tables.sql:58`); confirmei empiricamente contra `dev_gx_goldie` que o PDO deste ambiente devolve boolean como `bool` nativo → `(int)(true)===1`, `(int)(false)===0`. Logo `((int)$v)===0` distingue de fato os dois estados (não colapsa `'t'/'f'` em 0). A prova relatada pelo dev (sem o fix: unit FAIL=1 / E2E exit 1) é coerente com o código.

### m1 — ✅ RESOLVIDO
`Laboratorios/ReceberOrdemLaboratorio.php:166-168`: o retorno de `atualizarStatus('traduzida', …)` no caminho de sucesso passou a ser checado; falha loga `error_log` com `ordem_id`/`missao_id` para reconciliação (sem reverter a missão, que é legítima). Sem efeito colateral: os demais caminhos de `atualizarStatus` (rejeição) permanecem iguais, e o retorno `veredito(true, …, 'aceita')` continua correto (a missão existe).

### m2 — ✅ RESOLVIDO
`Tradutores/AlocacaoOrdemTradutor.php:140-145`: `resolverProdutoIds` agora loga (`error_log`) quando um `produto_ref` já validado a montante não resolve na tradução, em vez de omitir em silêncio. Comportamento funcional idêntico (item ainda omitido — correto p/ sessão livre); ganho puro de observabilidade.

### n1 / n2 — aceitos sem mudança
Documentados em `10-backend-fase3.md` como aceitos (leitura via V1 `Inteligencias` consistente com o domínio irmão; janela órfã de `localizarMissaoPorReferencia` de risco desprezível). Sem ação requerida.

### Regressões introduzidas pelos fixes — nenhuma
- O fix do B1 é uma chave a mais no `$data` do tradutor (opt-in), consumida por `GestaoMissoesBase:71`/`MissoesPersistencia:151`; não altera o fluxo nativo do painel (que manda o próprio valor) nem os outros tradutores (inexistentes). `GestaoMissoesAlocacaoWmsTddTest` 30/30 idêntico.
- m1/m2 são apenas checagem-de-retorno + log; nenhum caminho de dado mudou.
- Números relatados coerentes com o escopo: IntegracaoWms 337/0 (era 336; +1 = assert novo do tradutor 23→24), AlocacaoWms 348/12 (baseline pinada idêntica), GestaoMissoes 30/0, E2E 56/1 exit 0 (o 1 fail é o bug de infra pré-existente do GRUPO E, fora de escopo).

## Veredito final

✅ **Merge.** Os 3 achados acionáveis da 1ª passada (B1 blocker + m1/m2 minors) estão **corrigidos e verificados no código**, com o blocker guardado por regressão real em dois níveis (unit + E2E) que provei não ser falso-positivo. A decisão de B1 (ordem WMS sem aprovação) está documentada como desvio consciente. Nenhuma regressão introduzida; o fix ficou contido no tradutor (Fase 3), sem tocar arquivos compartilhados/alheios. Continua valendo, como observação **fora do escopo desta fase**, o bug de sessão do `WebhookEndpointSonda::findByChannel()` (Api/Sondas) — recomendo o smoke `curl` real contra o servidor antes do 1º WMS em produção multi-tenant, mas não bloqueia o merge desta entrega.
