# Implementação backend — Tradutores restantes (integracao-wms-desacoplada)

Escopo: techplan §Fase 3, passo 17 — os tradutores `movimentacao` e
`inventario`, um de cada vez, suíte verde entre cada um (mesma disciplina do
`alocacao` da entrega anterior). `separacao` **não foi implementado** —
bloqueio de decisão de produto, documentado abaixo e no README do sinal (§7.1).

Contexto lido antes de começar: `13-fixes-pos-fase3.md` (lookup global de
canal, `ReceberOrdemApiTest` 61/0) e o novo `README.md` do sinal.

## Ordem de entrega (cada um com suíte completa verde antes de seguir)

1. **Refactor prévio** (zero mudança de comportamento): extraí
   `OperacaoWmsOrdemTradutorBase` de `AlocacaoOrdemTradutor` — os dois
   próximos tradutores são quase idênticos a ele (mesma missão genérica,
   mesmo stub de `$galaxiaRoute`, mesmo B1, mesma forma de localizar a missão
   criada), então copiá-los à risca teria triplicado o risco de "esquecer o
   requer_aprovacao='0' numa cópia" — exatamente a classe de bug que o B1
   acabou de corrigir. Confirmado zero regressão: `AlocacaoOrdemTradutorTddTest`
   continua 24/24 sem editar o teste, suíte completa idêntica antes/depois.
2. **`movimentacao`** → `MovimentacaoOrdemTradutor` (`OperacaoTipo::MOVIMENTACAO`).
3. **`inventario`** → `InventarioOrdemTradutor` (`OperacaoTipo::INVENTARIO_PRODUTOS`/
   `INVENTARIO_ENDERECO`, resolvidos por `subtipo` explícito ou inferência).
4. **`separacao`** → **bloqueado**, ver "Bloqueio: separacao" abaixo.

## Arquivos criados

- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Tradutores/OperacaoWmsOrdemTradutorBase.php`
  — classe abstrata extraída do `AlocacaoOrdemTradutor` original: implementa
  `TraducaoOrdemInterface::traduzir()` (`final`) como template method —
  monta `$data` (incluindo `requer_aprovacao='0'`, `referencia_tipo=
  'WMS_ORDEM'`, `referencia_id`, `origem_wms`), despacha para
  `GestaoMissoesAlocacaoWms`, localiza a missão criada por
  `(referencia_tipo, referencia_id)` — e deixa só `montarSelecao(array
  $ordem): array` abstrato (retorna `operacao_tipo` + `itens_selecionados`, ou
  `ok=false` + `erro` pra rejeitar sem criar missão). Também concentra
  `resolverProdutoIds()` (produto_ref → produtos.id, já existia no
  `AlocacaoOrdemTradutor`) e a nova `resolverEnderecoIds()` (endereco_ref →
  wms_localizacoes.id, usada pelo escopo `inventario_endereco`). Mesmo
  racional de `AlocacaoWms/Strategies/Operacao/OperacaoAdapterBase.php`
  (defaults compartilhados + hooks abstratos para a diferença real).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Tradutores/MovimentacaoOrdemTradutor.php`
  — `montarSelecao()`: `operacao_tipo='movimentacao'` + produtos via
  `resolverProdutoIds()` (idêntico ao resolver da alocação). Sem itens é
  aceitável (sessão livre). `endereco_ref` origem/destino do envelope são só
  informativos nesta versão (não anexados ao item — mesma limitação já
  documentada para `alocacao`, ver `10-backend-fase3.md` desvio #4).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Tradutores/InventarioOrdemTradutor.php`
  — `montarSelecao()` resolve o **subtipo** (decisão desta entrega, ver
  "Decisão: subtipo do inventário" abaixo) e monta `operacao_tipo`/
  `itens_selecionados` de acordo; indeterminado → rejeita com motivo.
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Tradutores/MovimentacaoOrdemTradutorTddTest.php`
  (25 asserts) e `.../InventarioOrdemTradutorTddTest.php` (31 asserts) —
  espelham `AlocacaoOrdemTradutorTddTest` (mesma empresa/produtos fixture,
  block=22).

## Arquivos alterados

- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Tradutores/AlocacaoOrdemTradutor.php`
  — reduzido a `getOrdemTipo()` + `montarSelecao()`, estendendo
  `OperacaoWmsOrdemTradutorBase`. Comportamento idêntico (confirmado pela
  suíte, ver acima).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Tradutores/TraducaoOrdemFactory.php`
  — mapa ganha `'movimentacao' => MovimentacaoOrdemTradutor::class` e
  `'inventario' => InventarioOrdemTradutor::class`; comentário do `separacao`
  aponta para o bloqueio (não mais "próxima iteração").
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Tradutores/TraducaoOrdemFactoryTddTest.php`
  — generalizado para um mapa `tipo => classe esperada|null`, testado em
  loop (evita reescrever o arquivo inteiro a cada tradutor plugado); adiciona
  os `require_once` de `OperacaoWmsOrdemTradutorBase`/cada tradutor
  concreto (necessários porque o arquivo não usa `autoload.php` — `extends`
  resolve no load da classe, diferente de dependências usadas só em runtime).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Laboratorios/ReceberOrdemLaboratorioTddTest.php`
  — seção 6 ("tipo conhecido sem tradutor") migrada duas vezes seguindo os
  tradutores plugados: `movimentacao` → `inventario` → `separacao` (única
  pendente agora). Ver "Armadilha conhecida" abaixo.
- `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php` — B2/B3 e GRUPO C
  migrados na mesma lógica (ver "Armadilha conhecida"); GRUPO F (novo, espelha
  D) cobre `movimentacao` ponta a ponta via HTTP real (F1 aceite+missão, F2
  dedup, F3 ciclo de report); GRUPO G (novo) cobre `inventario` (subtipo=
  'endereco' explícito, sem itens — evita depender de fixtures de
  produto/endereço na empresa 7 de dev).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/README.md` — §1 (mapa de
  diretórios), §3 (status), §4 (contrato — `subtipo`), §6 (checklist), §7
  (receita genérica via `OperacaoWmsOrdemTradutorBase` + nova §7.1 com o
  bloqueio de `separacao` documentado in-place), §9 (testes).

## Decisões tomadas

### Decisão: `subtipo` do inventário

Ordem `tipo='inventario'` resolve o escopo nesta prioridade:
1. **`$ordem['subtipo']` explícito** (`'produtos'|'endereco'`) — fonte da
   verdade quando informado; vence mesmo que os itens, sozinhos, seriam
   ambíguos (testado: `InventarioOrdemTradutorTddTest` caso 8).
2. **Inferência pelos itens**, quando `subtipo` ausente: item com
   `produto_ref`/`importacao_id` (endereco_ref junto, se houver, é só
   informativo — mesma regra de alocação/movimentação) → `'produtos'`; item
   **só** com `endereco_ref`/`localizacao_codigo` (sem produto_ref) →
   `'endereco'`. Misturar os dois padrões no mesmo envelope sem `subtipo`
   explícito é **ambíguo**.
3. **Indeterminado** (sem `subtipo`, sem itens, ou itens ambíguos) → **rejeita
   a ordem inteira** com motivo claro ("inventário precisa de escopo"). Não
   existe aqui um "modo livre" equivalente ao de `alocacao` (que aceita
   seleção vazia): sem saber se o escopo é produto ou endereço, o
   `operacao_tipo` da missão (e portanto a tela/fluxo de execução do
   operador) não é determinável — criar a missão seria um chute.

`'produtos'` → `operacao_tipo='inventario_produtos'`, seleção = produtos
(mesmo resolver de alocação/movimentação — `GestaoMissoesAlocacaoWms::buscarItens()`
trata igual). `'endereco'` → `operacao_tipo='inventario_endereco'`, seleção =
`wms_localizacoes.id` (resolvidos de `endereco_ref` via
`WmsLocalizacaoSonda::buscarPorCodigo()`) — `buscarItens()` **ignora**
completamente essa seleção para essa operação (a missão nasce sem itens de
produto, descobertos na execução); o que realmente importa é
`configuracoes.inventario_alvos`, montado por
`GestaoMissoesAlocacaoWms::resolverAlvosEndereco()` a partir dos MESMOS ids
que passamos em `itens_selecionados` — confirmado por leitura do código antes
de escrever o tradutor (não há outro jeito de alimentar essa config).

### Decisão: `endereco_ref` de origem/destino em `movimentacao` é só informativo

Igual ao desvio #4 já documentado para `alocacao` (`10-backend-fase3.md`): a
operação de movimentação, como a de alocação, é endereço-primeiro — o
operador escolhe origem e destino durante a execução
(`ConfirmarAlocacaoLaboratorio`/`MovimentacaoAdapter`), não na criação da
missão. Não existe hoje um campo de item da missão para pré-fixar essas
referências; a identidade dos endereços já foi confirmada por
`OrdemIdentidadeValidator` antes de a ordem chegar ao tradutor.

## Bloqueio: `separacao` (NÃO implementado — decisão de produto pendente)

Investiguei antes de descartar — não é falta de tempo, é uma incompatibilidade
estrutural real entre o contrato de entrada e como `GestaoMissoesSeparacao`
funciona:

- `GestaoMissoesSeparacao::buscarItens()` herda
  `GestaoMissoesBase::recuperarItensSelecionados()`, que resolve **sempre**
  via `romaneio_id`/`pedido_id` → `pedidos_produtos` (modo hierárquico ou
  plano). Diferente de Alocação/Movimentação/Inventário-produtos (que aceitam
  uma lista de `produto_id` direta, sem depender de pedido/romaneio),
  Separação **não tem** um modo "produto avulso" equivalente.
- O contrato v1 da ordem WMS (`external_ref`, `tipo`, `itens: [{produto_ref,
  endereco_ref, quantidade}]`) **não carrega nenhuma referência a
  pedido/romaneio** — só identidade de produto/endereço (AC5.1/5.2 da spec,
  `01-spec.md`). Não há de onde tirar o `romaneio_id`/`pedido_id` que
  `GestaoMissoesSeparacao` exige para montar os itens.

Ou seja: para destravar `separacao`, ou o contrato de entrada ganha uma
referência a pedido/romaneio (um mapeamento de identidade NOVO, que a spec
atual não cobre — AC5.1/5.2 só falam de produto/endereço), ou
`GestaoMissoesSeparacao` ganha um modo de seleção direta por produto (mudança
de escopo maior, em lógica de negócio compartilhada com o painel de
Designações — não é uma extensão opt-in de 1-2 linhas como as que os outros 3
tradutores usaram). Nenhuma das duas é uma decisão técnica que eu deva tomar
sozinho.

**Pergunta exata a decidir** (produto + Tech Lead — registrada também em
`README.md` §7.1): o WMS, ao mandar uma ordem `tipo='separacao'`, referencia
um pedido/romaneio do Genesis (por qual identificador?), ou a intenção é
picking **sem** pedido associado — caso em que talvez o destino certo nem
seja `TipoMissao::SEPARACAO`/`GestaoMissoesSeparacao`, e sim mais uma operação
de `GestaoMissoesAlocacaoWms` (a mesma família dos outros 3), com o nome
`separacao` no contrato precisando de outra definição?

Enquanto isso, `tipo='separacao'` continua **rejeitado com motivo** (AC4.4) —
comportamento seguro herdado do Factory de mapa fechado, idêntico ao que
`movimentacao`/`inventario` tinham antes de serem plugados. Nenhuma missão
"meio pronta", nenhum código especulativo escrito.

## Armadilha conhecida dos testes (documentada no próprio código, seguida à risca)

`ReceberOrdemLaboratorioTddTest` (seção 6) e `ReceberOrdemApiTest` (B2/B3 +
GRUPO C) precisam de pelo menos 1 `tipo` **sem** tradutor para testar o
caminho de rejeição. Migrei os dois arquivos a cada tradutor plugado:
`movimentacao` (rejeitado) → `inventario` (rejeitado) → agora `separacao` é o
único tipo restante nessa posição. Cada arquivo tem um comentário
"ARMADILHA CONHECIDA" explicando a situação e o próximo passo (se
`separacao` for desbloqueado, trocar para um tipo fora do enum fechado —
`OrdemTipo::tryFrom()` retorna `null` — e documentar que o ramo "registrado
sem tradutor" ficou sem representante real).

GRUPO C do `ReceberOrdemApiTest` também precisou de ajuste de fundo: como
`movimentacao` passou a ter tradutor, a chamada da empresa 7 (que sempre
usou esse tipo) virou um **aceite real** (200, cria missão) no meio de um
teste que antes só testava rejeição (422) — os asserts de status mudaram
(`422→200` para emp7) e adicionei verificação de que a missão criada
pertence ao tenant correto (não vaza para a empresa de teste) — o cleanup
genérico do `finally` (por `external_ref LIKE '__test_apiflow%'`) já cobria a
missão nova sem precisar de código extra.

## Comandos de verificação executados

`php -l` em todo PHP novo/alterado desta entrega → **OK** em todos (sweep
completo do sinal + o arquivo de integração, 0 erros).

### Suítes por tradutor (dev_gx_goldie) — números reais, antes e depois de cada um

**Baseline no início desta entrega** (após os fixes pós-Fase 3): suíte
unit do sinal (12 arquivos) 337/0; `ReceberOrdemApiTest` 61/0;
`ConfigIntegracaoTest` 47/0; `Fase2MigrationsTest` 41/0.

**1. Refactor (`OperacaoWmsOrdemTradutorBase`, zero mudança de comportamento)**
- `AlocacaoOrdemTradutorTddTest`: 24/0 antes → 24/0 depois (idêntico).
- Suíte unit completa (12 arquivos, após corrigir o `require_once` que faltava
  em `TraducaoOrdemFactoryTddTest`): 337/0 → **319/0** temporariamente durante
  o fix do require (1 arquivo com erro fatal de "classe não encontrada",
  corrigido antes de prosseguir) → **337/0** confirmado após o fix.
- `ReceberOrdemApiTest`: 61/0 (inalterado).

**2. `movimentacao`**
- `MovimentacaoOrdemTradutorTddTest` (novo): **25/0**.
- `TraducaoOrdemFactoryTddTest`: 18/0 → **19/0** (+1, `movimentacao` não lança mais).
- `ReceberOrdemLaboratorioTddTest`: precisou migrar a seção 6 (`movimentacao`→`inventario`)
  — sem a migração, 6 asserts quebravam (o tipo passou a ser aceito, não mais
  rejeitado); com a migração: **65/0** (era 66/0 antes de qualquer tradutor
  novo — a diferença de 1 é só o loop de "outros tipos sem tradutor" perder
  uma iteração, não uma regressão).
- Suíte unit completa (13 arquivos, com o novo teste): **362/0**.
- `ReceberOrdemApiTest`: precisou migrar B2/B3 (`movimentacao`→`inventario`) e
  GRUPO C (emp7 passa a aceitar/criar missão); GRUPO F novo (espelha D):
  **87/0** (61 base + 26 do GRUPO F/ajustes).
- Resíduo no banco: zero (checado após cada rodada).

**3. `inventario`**
- `InventarioOrdemTradutorTddTest` (novo): **31/0**.
- `TraducaoOrdemFactoryTddTest`: 19/0 → **20/0** (+1).
- `ReceberOrdemLaboratorioTddTest`: seção 6 migrada de novo (`inventario`→`separacao`,
  a única pendente agora) — sem migrar, 3 asserts quebravam (motivo real virou
  o de "inventário sem escopo", não mais "sem tradutor", já que `inventario`
  sem itens rejeita por um motivo DIFERENTE agora — uma pegadinha a mais que
  `movimentacao` não tinha, documentada no código): **65/0** confirmado.
- Suíte unit completa (14 arquivos, com o novo teste): **393/0**.
- `ReceberOrdemApiTest`: B2/B3 e GRUPO C migrados de `inventario` para
  `separacao` (o `inventario` sem itens rejeitaria por motivo diferente do
  esperado pelo teste — mesma pegadinha acima); GRUPO G novo (espelha D/F,
  `subtipo='endereco'` explícito sem itens): **108/0**.
- Resíduo no banco: zero.

### Verificação final consolidada (após os 2 tradutores + toda a migração de testes)

| Suíte | Resultado |
|---|---|
| Unit do sinal IntegracaoWms (14 arquivos) | **393 PASS / 0 FAIL** |
| `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php` | **108 PASS / 0 FAIL** (0 bugs documentados) |
| `tests/integration/IntegracaoWms/ConfigIntegracaoTest.php` | 47 PASS / 0 FAIL (inalterado) |
| `tests/integration/IntegracaoWms/Fase2MigrationsTest.php` | 41 PASS / 0 FAIL (inalterado) |
| AlocacaoWms (27 arquivos, domínio consumido só por leitura) | 348 PASS / 12 FAIL — **idêntico à baseline pré-existente** (mesmos arquivos/motivos: `WmsProviderRegistry` não bootstrapado fora de `Api\Bootstrap` + causa não investigada em `ConfirmarAlocacaoValidatorTddTest`, ambos fora do escopo desta entrega) |
| `Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php` | 30 PASS / 0 FAIL (inalterado, confirma que o refactor da base não quebrou o consumidor original) |

Resíduo no banco (`wms_ordens`/`missao_logistica` com marca de teste/`WMS_ORDEM`/
`wms_localizacoes` com prefixo `ZZ-TEST`): **zero**, checado após cada rodada de
testes.

## Nota sobre commits

Esta entrega **não foi commitada por mim** (nenhum `git commit` foi
executado nesta sessão). Ao final do trabalho, `git status`/`git log`
mostraram que os arquivos já estão presentes no HEAD do branch `dev_team`,
aparentemente capturados por um mecanismo de checkpoint automático do
ambiente sob mensagens de commit de uma feature não relacionada
(`separacao-endereco-wms`) — confirmado por `git diff HEAD` vazio para os
arquivos desta entrega (conteúdo idêntico ao que escrevi, sem perda/corrupção).
Registro aqui só por transparência; não tomei nenhuma ação de commit/push.

## Próximos passos para o frontend

Inalterado: nenhum novo endpoint de UI. Ordens `movimentacao`/`inventario`
aceitas criam missões `TipoMissao::ALOCACAO_WMS` que aparecem no app do
operador exatamente como as nativas — a única distinção é
`configuracoes.operacao_tipo` (já usado hoje pela tela de Operação WMS para
escolher o fluxo certo) e `referencia_tipo='WMS_ORDEM'`/`configuracoes.origem='wms'`
para quem quiser filtrar por origem.
