# Testes — Fase 2 (Receiver de entrada + tradução scaffold + idempotência + ciclo de report)

Escopo testado: passos 8-15 do `02-techplan.md` conforme implementados e reportados em
`07-backend-fase2.md`. Cobertura em dois níveis, seguindo o padrão V2 do repo:

- **TDD unit/integração do sinal** (`componentes/.../IntegracaoWms/Testes/`) — scripts
  standalone, alguns puros (sem banco), outros contra `dev_gx_goldie`.
- **Integration** (`tests/integration/IntegracaoWms/`) — migrations + fluxo HTTP
  ponta-a-ponta via `InboundHandler` real.

Todos os arquivos rodam com `php <caminho>.php` (sem flags, sem phpunit).

## Cobertura

| Componente | Tipo | Path | Status |
|---|---|---|---|
| `WmsPayloadNormalizer` (versão v1/v99, alias `contrato_versao`) | unit puro | `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Webhooks/WmsPayloadNormalizerTddTest.php` | ✅ pass (14/14) |
| `OrdemTipo` + `TraducaoOrdemFactory` (enum fechado, mapa vazio, lança p/ os 4 tipos) | unit puro | `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Tradutores/TraducaoOrdemFactoryTddTest.php` | ✅ pass (17/17) |
| `OrdemEnvelopeValidator` (campos obrigatórios, `itens` deve ser array) | unit puro | `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Validators/OrdemEnvelopeValidatorTddTest.php` | ✅ pass (15/15) |
| `OrdemIdentidadeValidator` (produto/endereço conhecidos, aliases, endereço inativo) | TDD integração (DB) | `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Validators/OrdemIdentidadeValidatorTddTest.php` | ✅ pass (20/20) |
| `WmsOrdemSonda` (dedup, criar/atualizarStatus/buscarPorMissaoId, isolamento por empresa) | TDD integração (DB) | `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Sondas/WmsOrdemSondaTddTest.php` | ✅ pass (35/35) |
| `ReceberOrdemLaboratorio` (fluxo completo: envelope→tenant→ativo_entrada→dedup→identidade→tradução) | TDD integração (DB) | `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Laboratorios/ReceberOrdemLaboratorioTddTest.php` | ✅ pass (38/38) |
| `ReportarOrdemLaboratorio` (no-op nativo, órfã, idempotente, pendente, avança c/ push enviado) | TDD integração (DB) | `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Laboratorios/ReportarOrdemLaboratorioTddTest.php` | ✅ pass (18/18) |
| Migration `002_create_wms_ordens.sql` (schema, índices, FK) | integration | `tests/integration/IntegracaoWms/Fase2MigrationsTest.php` | ✅ pass (41/41) |
| Migration `003_seed_webhook_endpoint_dev.sql` (canal `wms-7`, config `ativo_entrada=1`) | integration | `tests/integration/IntegracaoWms/Fase2MigrationsTest.php` (mesmo arquivo) | ✅ pass |
| `POST /api/webhooks/wms-7` ponta-a-ponta via `InboundHandler` (versão inválida, sem tradutor, dedup, HMAC inválido, canal inexistente) | integration | `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php` | ✅ pass (31/31) — bug do roteamento **corrigido** (ver abaixo), Grupo A virou guard de não-regressão |
| Cross-tenant via HTTP (2 canais, mesmo `external_ref`, sem colisão) | integration | `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php` (Grupo C) | ✅ pass |
| Resposta HTTP rica pós-fix do `InboundHandler` (`ok/motivo/status`, HTTP 422 quando `ok=false`) | integration | `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php` (Grupo B) | ✅ pass — fix validado |

### Baseline de não-regressão (Fase 1 — reexecutada, nada quebrou)

| Componente | Path | Status |
|---|---|---|
| `GalpaoProviderParidadeTddTest` | `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Provedores/GalpaoProviderParidadeTddTest.php` | ✅ pass (35/35) |
| `WmsProviderRegistryTddTest` | `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Provedores/WmsProviderRegistryTddTest.php` | ✅ pass (27/27) |
| `IntegracaoWmsConfigSondaTddTest` | `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Config/IntegracaoWmsConfigSondaTddTest.php` | ✅ pass (21/21) |
| `AlocacaoHookStrategyTddTest` | `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Strategies/AlocacaoHookStrategyTddTest.php` | ✅ pass (41/41) |
| `ConfigIntegracaoTest` | `tests/integration/IntegracaoWms/ConfigIntegracaoTest.php` | ✅ pass (47/47) |

### Contagem total

| Categoria | Arquivos | Asserções | Passing | Failing (bugs reais) |
|---|---|---|---|---|
| Unit puro (sem banco) — novos Fase 2 | 3 | 46 | 46 | 0 |
| TDD integração (DB) dentro do sinal — novos Fase 2 | 4 | 111 | 111 | 0 |
| Integration (`tests/integration/`) — novos Fase 2 | 2 | 72 | 72 | 0 |
| Baseline Fase 1 (reexecutada, não-regressão) | 5 | 171 | 171 | 0 |
| **Total geral** | **14** | **400** | **400** | **0** |

> Atualização pós-fix: o bug de roteamento (canal `wms-7` inalcançável, ver
> "Bugs encontrados") foi corrigido pelo orquestrador em `Migrations/003_seed_webhook_endpoint_dev.sql`
> logo após esta rodada. `ReceberOrdemApiTest.php` foi ajustado (Grupo A deixou de
> reproduzir o bug e virou guard de não-regressão) e **toda a suíte foi
> re-executada do zero, 400/400 passing, 0 bugs**.

`php -l` sem erros em todos os 9 arquivos de teste novos e nos arquivos de produção
tocados nesta fase (`InboundHandler.php`, `crons/wms_alocacao_integracao.php`,
`Provedores/Http/HttpWmsProvider.php`).

## Gaps de cobertura (e justificativa)

- **`HttpWmsProvider` (POST real com HMAC out)** — não coberto por teste novo nesta
  rodada. É extração/evolução do `WmsEnvioContexto`/`WmsEnvioResultado` já cobertos
  pela suíte de Fase 1 (`GalpaoProviderParidadeTddTest` cobre o padrão de VO/erro de
  rede); o provedor `http_wms` em si só é exercitado ponta-a-ponta quando um tradutor
  concreto e um WMS real existirem (Fase 3+). Testar aqui exigiria mockar uma porta
  HTTP local — fora do escopo desta rodada (nenhum consumidor real do provedor ainda).
- **Tradutores concretos (`AlocacaoOrdemTradutor` etc.)** — não existem nesta fase
  (mapa vazio de propósito); não há o que testar além do que já é coberto por
  `TraducaoOrdemFactoryTddTest` (garante que `create()` lança para os 4 tipos).
- **`crons/wms_alocacao_integracao.php` (integração do bloco isolado)** — não escrevi
  um teste específico do arquivo do cron. Conforme a instrução do orquestrador, o
  bloco novo só chama `ReportarOrdemLaboratorio::reportar($missaoId)` num loop com
  try/catch (baixo risco, sem lógica de negócio própria) — a cobertura real está em
  `ReportarOrdemLaboratorioTddTest.php`, que testa exatamente esse método isolado com
  todos os cenários relevantes. Não toquei nas partes do arquivo que pertencem à
  feature de aprovação/estorno (fora do escopo, não commitada).
- **Tela de config (`html/integracaowms.php`)** — sem mudança nesta fase (confirmado
  no `07-backend-fase2.md`); não há CSS/visual a testar (fora do escopo do Tester de
  qualquer forma).
- **UI/telas de observabilidade da ordem** — não existem ainda (não pedidas nesta fase).

## Output da execução

### Unit puro — `WmsPayloadNormalizerTddTest.php`
```
====================================================
UNIT — WmsPayloadNormalizer (sem banco)
====================================================

--- version='v1' ---
[PASS] version='v1' → sem _envelope_invalido
[PASS] version='v1' → contrato_versao setado para 'v1'
[PASS] version='v1' → demais campos do payload preservados (external_ref)
[PASS] version='v1' → demais campos do payload preservados (tipo)

--- version ausente (default) ---
[PASS] version ausente → default 'v1' → sem _envelope_invalido
[PASS] version ausente → contrato_versao default = 'v1'

--- alias contrato_versao ---
[PASS] alias contrato_versao='v1' → sem _envelope_invalido
[PASS] alias contrato_versao='v1' → preservado

--- version desconhecida ('v99') ---
[PASS] version desconhecida → NÃO lança exceção
[PASS] version='v99' → _envelope_invalido=true
[PASS] version='v99' → _motivo menciona a versão recebida
[PASS] version='v99' → _motivo lista as versões suportadas ('v1')

--- version='' (string vazia) ---
[PASS] version='' → _envelope_invalido=true (string vazia não é 'v1')

--- version='V1' (maiúscula) — caracterização ---
[PASS] version='V1' (maiúscula) → rejeitada (comparação case-sensitive, in_array estrito)

====================================================
RESULTADO: PASS=14  FAIL=0
TODOS OS TESTES PASSARAM
```

### Unit puro — `TraducaoOrdemFactoryTddTest.php`
```
====================================================
UNIT — TraducaoOrdemFactory / OrdemTipo (sem banco)
====================================================

--- OrdemTipo: 4 casos fechados ---
[PASS] OrdemTipo::cases() tem exatamente 4 casos
[PASS] OrdemTipo::SEPARACAO->value === 'separacao'
[PASS] OrdemTipo::MOVIMENTACAO->value === 'movimentacao'
[PASS] OrdemTipo::ALOCACAO->value === 'alocacao'
[PASS] OrdemTipo::INVENTARIO->value === 'inventario'

--- OrdemTipo::tryFrom() ---
[PASS] tryFrom('alocacao') resolve o enum
[PASS] tryFrom('recebimento') [tipo não existente] → null
[PASS] tryFrom('') → null

--- TraducaoOrdemFactory::create() lança para os 4 tipos (Fase 2) ---
[PASS] create(OrdemTipo::SEPARACAO) lança InvalidArgumentException
[PASS] create(OrdemTipo::SEPARACAO) mensagem menciona 'separacao'
[PASS] create(OrdemTipo::MOVIMENTACAO) lança InvalidArgumentException
[PASS] create(OrdemTipo::MOVIMENTACAO) mensagem menciona 'movimentacao'
[PASS] create(OrdemTipo::ALOCACAO) lança InvalidArgumentException
[PASS] create(OrdemTipo::ALOCACAO) mensagem menciona 'alocacao'
[PASS] create(OrdemTipo::INVENTARIO) lança InvalidArgumentException
[PASS] create(OrdemTipo::INVENTARIO) mensagem menciona 'inventario'

--- tiposSuportados() ---
[PASS] tiposSuportados() retorna array vazio (Fase 2 — nenhum tradutor plugado)

====================================================
RESULTADO: PASS=17  FAIL=0
TODOS OS TESTES PASSARAM
```

### Unit puro — `OrdemEnvelopeValidatorTddTest.php`
```
====================================================
UNIT — OrdemEnvelopeValidator (sem banco)
====================================================

--- Caminho feliz ---
[PASS] envelope completo → ok=true
[PASS] itens ausente (opcional) → ok=true
[PASS] itens=[] (array vazio) → ok=true

--- external_ref obrigatório ---
[PASS] sem external_ref → ok=false
[PASS] sem external_ref → motivo menciona o campo
[PASS] external_ref só com espaços (trim → '') → ok=false
[PASS] external_ref numérico (cast string) → ok=true (tolerante a tipo)

--- tipo obrigatório ---
[PASS] sem tipo → ok=false
[PASS] sem tipo → motivo menciona o campo
[PASS] tipo='' → ok=false
[PASS] tipo com valor fora do enum fechado → ok=true (Validator só checa presença; enum é responsabilidade do Laboratorio)

--- itens deve ser array ---
[PASS] itens='nao-e-array' (string) → ok=false
[PASS] itens inválido → motivo menciona o campo
[PASS] itens=42 (número) → ok=false
[PASS] itens=null → tratado como ausente (isset()===false) → ok=true

====================================================
RESULTADO: PASS=15  FAIL=0
TODOS OS TESTES PASSARAM
```

### TDD integração (DB) — `OrdemIdentidadeValidatorTddTest.php`
```
====================================================
INTEGRATION — OrdemIdentidadeValidator (dev_gx_goldie)
====================================================

--- Sem itens ---
[PASS] data sem chave 'itens' → ok=true
[PASS] itens=[] → ok=true
[PASS] itens não-array (já barrado pelo OrdemEnvelopeValidator antes) → ok=true (defensivo)

--- Produto conhecido (produto_ref) ---
[PASS] produto_ref conhecido → ok=true
[PASS] alias 'importacao_id' no lugar de 'produto_ref' → ok=true

--- Produto desconhecido ---
[PASS] produto_ref desconhecido → ok=false
[PASS] motivo cita a referência desconhecida
[PASS] motivo aponta o índice do item (#0)
[PASS] motivo menciona "produto" (não vaza detalhe interno confuso)
[PASS] segundo item com produto desconhecido → ok=false
[PASS] motivo aponta o índice correto (#1), não o primeiro item (válido)

--- Endereço conhecido (endereco_ref, ativo) ---
[PASS] endereco_ref ativo e conhecido → ok=true
[PASS] alias 'localizacao_codigo' no lugar de 'endereco_ref' → ok=true

--- Endereço desconhecido ---
[PASS] endereco_ref desconhecido → ok=false
[PASS] motivo cita a referência desconhecida
[PASS] motivo menciona "endereço"

--- Endereço inativo (ativo=0) ---
[PASS] endereço existente mas INATIVO → ok=false (mesmo tratamento de desconhecido)

--- Item sem produto_ref nem endereco_ref ---
[PASS] item só com quantidade (sem refs) → ok=true

--- Item misto: produto OK + endereço desconhecido ---
[PASS] produto OK mas endereço desconhecido no mesmo item → ok=false (rejeita a ordem inteira)

--- Múltiplos itens válidos ---
[PASS] todos os itens válidos → ok=true

[CLEANUP] Fixtures removidas.

====================================================
RESULTADO: PASS=20  FAIL=0
TODOS OS TESTES PASSARAM
```

### TDD integração (DB) — `WmsOrdemSondaTddTest.php`
```
====================================================
INTEGRATION — WmsOrdemSonda (dev_gx_goldie)
====================================================

--- buscarPorExternalRef() antes de criar ---
[PASS] buscarPorExternalRef() antes da 1ª ordem → null
[PASS] buscarPorExternalRef(empresaId=0) → null (guard)
[PASS] buscarPorExternalRef(externalRef='') → null (guard)

--- criar() ---
[PASS] criar() retorna um id inteiro > 0
[PASS] buscarPorExternalRef() encontra a ordem recém-criada
[PASS] ordem encontrada tem o mesmo id retornado por criar()
[PASS] ordem gravada: tipo = "alocacao"
[PASS] ordem gravada: status = "rejeitada"
[PASS] ordem gravada: motivo preservado
[PASS] ordem gravada: block_empresa_id correto
[PASS] ordem gravada: contrato_versao = "v1"
[PASS] ordem gravada: payload_bruto não vazio (JSONB persistido)
[PASS] criar() sem status explícito → default "recebida"
[PASS] criar() sem contrato_versao explícito → default "v1"
[PASS] criar(empresaId=0) → null (guard)

--- atualizarStatus() ---
[PASS] atualizarStatus() → true
[PASS] atualizarStatus(): status avançou para "traduzida"
[PASS] atualizarStatus() sem motivo novo → motivo anterior preservado
[PASS] atualizarStatus("reportada"): status correto
[PASS] atualizarStatus(): motivo sobrescrito quando informado
[PASS] atualizarStatus(): missao_logistica_id vinculado
[PASS] atualizarStatus("reportada"): reportada_em preenchido automaticamente
[PASS] atualizarStatus(id=0) → false (guard)

--- buscarPorMissaoId() ---
[PASS] buscarPorMissaoId() encontra a ordem vinculada
[PASS] buscarPorMissaoId(): id bate com a ordem esperada
[PASS] buscarPorMissaoId(0) → null (guard)
[PASS] buscarPorMissaoId(missão inexistente) → null

--- Isolamento por block_empresa_id (cross-tenant) ---
[PASS] mesmo external_ref em DUAS empresas → ambas as linhas coexistem (dedup é por empresa)
[PASS] ordem da empresa A tem block_empresa_id = A
[PASS] ordem da empresa B tem block_empresa_id = B
[PASS] cada empresa vê APENAS o próprio registro (tipos diferentes não se confundem)
[PASS] são linhas fisicamente distintas (ids diferentes)
[PASS] empresa A não enxerga o motivo gravado pela empresa B (isolamento)

--- UNIQUE(block_empresa_id, external_ref): reenvio dentro da MESMA empresa ---
[PASS] criar() com external_ref já existente NA MESMA empresa → null (constraint UNIQUE barra a duplicata)
[PASS] exatamente 1 linha para (empresa A, external_ref cross) — sem duplicata física no banco

[CLEANUP] Linhas de teste removidas.

====================================================
RESULTADO: PASS=35  FAIL=0
TODOS OS TESTES PASSARAM
```

### TDD integração (DB) — `ReceberOrdemLaboratorioTddTest.php`
```
====================================================
INTEGRATION — ReceberOrdemLaboratorio (dev_gx_goldie)
====================================================

--- 0. Envelope inválido (versão desconhecida) ---
[PASS] envelope inválido → ok=false
[PASS] envelope inválido → status="rejeitada"
[PASS] envelope inválido → ordem_id=null (nenhuma linha gravada)
[PASS] envelope inválido → motivo repassado do Normalizer
[PASS] envelope inválido → NENHUMA linha nova em wms_ordens (tenant nem foi resolvido)

--- 1. Canal não resolvido ---
[ReceberOrdemLaboratorio] Não foi possível resolver empresa a partir do canal 'canal-invalido'.
[PASS] canal não resolvido → ok=false
[PASS] canal não resolvido → motivo menciona "Canal"
[PASS] canal não resolvido → ordem_id=null

--- 2. ativo_entrada=0 (empresa desligada) ---
[PASS] ativo_entrada=0 → ok=false
[PASS] ativo_entrada=0 → motivo menciona "desativada"
[PASS] ativo_entrada=0 → NÃO grava wms_ordens (rejeitada antes do envelope validator)

--- 3. Envelope: campo obrigatório ausente ---
[PASS] sem external_ref → ok=false
[PASS] sem external_ref → motivo menciona o campo
[PASS] sem external_ref → ordem_id=null (nem chega a existir external_ref pra gravar)

--- 4. Identidade desconhecida (produto/endereço) ---
[PASS] identidade desconhecida → ok=false
[PASS] identidade desconhecida → status="rejeitada"
[PASS] identidade desconhecida → motivo do Validator repassado
[PASS] identidade desconhecida → ordem_id gravado (persistiu a rejeição)
[PASS] identidade desconhecida → linha existe em wms_ordens
[PASS] linha persistida: status="rejeitada"
[PASS] linha persistida: tipo="alocacao"

--- 5. Tipo de ordem desconhecido (fora do enum) ---
[PASS] tipo fora do enum → ok=false
[PASS] tipo fora do enum → motivo menciona "desconhecido"

--- 6. Tipo conhecido sem tradutor (Fase 2) ---
[PASS] tipo válido sem tradutor → ok=false
[PASS] tipo válido sem tradutor → status="rejeitada"
[PASS] motivo contém "Tipo não suportado nesta versão" (prefixo real do catch)
[PASS] motivo contém "sem tradutor implementado" (mensagem da exceção do Factory)
[PASS] motivo cita o tipo concreto da ordem ("alocacao")
[PASS] tipo='separacao' também rejeitado por falta de tradutor (Fase 2 cobre os 4 tipos igualmente)
[PASS] tipo='movimentacao' também rejeitado por falta de tradutor (Fase 2 cobre os 4 tipos igualmente)
[PASS] tipo='inventario' também rejeitado por falta de tradutor (Fase 2 cobre os 4 tipos igualmente)

--- 7. Dedup: reenvio do mesmo external_ref ---
[PASS] pré-condição: exatamente 1 linha gravada no passo 6 para este external_ref
[PASS] reenvio → status="ja_processada" (idempotente)
[PASS] reenvio de uma ordem que foi rejeitada → ok=false (preserva o veredito anterior, não mascara como sucesso)
[PASS] reenvio → motivo é EXATAMENTE o mesmo texto da rejeição original (não recalcula)
[PASS] reenvio → mesmo ordem_id da 1ª vez
[PASS] reenvio → NÃO duplica linha em wms_ordens (ainda exatamente 1)
[PASS] dedup por external_ref independe do conteúdo do reenvio (tipo diferente ainda é "ja_processada")

[CLEANUP] Linhas de teste removidas.

====================================================
RESULTADO: PASS=38  FAIL=0
TODOS OS TESTES PASSARAM
```

### TDD integração (DB) — `ReportarOrdemLaboratorioTddTest.php`
```
====================================================
INTEGRATION — ReportarOrdemLaboratorio (dev_gx_goldie)
====================================================

--- 1. Missão nativa (referencia_tipo != 'WMS_ORDEM') ---
[PASS] fixture: missão nativa criada
[PASS] reportar(missão nativa) → false (no-op seguro)
[PASS] reportar(missão com referencia_tipo diferente de 'WMS_ORDEM') → false

--- 2. Missão WMS_ORDEM sem wms_ordens vinculada ---
[ReportarOrdemLaboratorio] missão #3169 marcada WMS_ORDEM mas sem wms_ordens vinculada.
[PASS] reportar(missão WMS_ORDEM sem linha em wms_ordens) → false (nada pra avançar)

--- 3. Ordem já 'reportada' (idempotente) ---
[PASS] fixture: ordem já-reportada criada
[PASS] reportar(ordem já 'reportada') → true (idempotente, sem re-processar)
[PASS] reportar() chamado 2x seguidas na mesma missão já-reportada → true nas duas

--- 4. Push ainda pendente (sem 'enviado' em nenhum canal) ---
[PASS] reportar(push ainda 'pendente') → false (próximo ciclo do cron tenta de novo)
[PASS] ordem permanece "traduzida" (não avançou precocemente)

--- 5. Push confirmado 'enviado' (ERP) → avança pra 'reportada' ---
[PASS] fixture: movimento confirmado criado
[PASS] reportar(push ERP 'enviado') → true (avança o ciclo)
[PASS] ordem avançou pra status="reportada"
[PASS] reportada_em preenchido
[PASS] reportar() 2ª vez (já reportada) → continua true, idempotente

--- 5b. Push confirmado 'enviado' só no canal WMS ---
[PASS] reportar(push WMS 'enviado', ERP ainda pendente) → true (não exige os DOIS canais)

--- 6. Guard: missaoId inválido ---
[PASS] reportar(0) → false
[PASS] reportar(-5) → false
[PASS] reportar(missão inexistente) → false (findById vazio)

[CLEANUP] Fixtures removidas.

====================================================
RESULTADO: PASS=18  FAIL=0
TODOS OS TESTES PASSARAM
```

### Integration — `Fase2MigrationsTest.php`
```
====================================================
INTEGRATION — IntegracaoWms Fase 2: Migrations 002+003 (dev_gx_goldie)
====================================================

--- Migration 002: wms_ordens ---
[PASS] Migration 002 lida do disco
[INFO] Migration 002 aplicada (idempotente).
[PASS] tabela wms_ordens existe
[PASS] coluna 'id' existe em wms_ordens
[PASS] coluna 'external_ref' existe em wms_ordens
[PASS] coluna 'tipo' existe em wms_ordens
[PASS] coluna 'contrato_versao' existe em wms_ordens
[PASS] coluna 'status' existe em wms_ordens
[PASS] coluna 'missao_logistica_id' existe em wms_ordens
[PASS] coluna 'motivo' existe em wms_ordens
[PASS] coluna 'payload_bruto' existe em wms_ordens
[PASS] coluna 'recebida_em' existe em wms_ordens
[PASS] coluna 'reportada_em' existe em wms_ordens
[PASS] coluna 'block_empresa_id' existe em wms_ordens
[PASS] coluna 'block_filial_id' existe em wms_ordens
[PASS] coluna 'block_usuario_id' existe em wms_ordens
[PASS] coluna 'block' existe em wms_ordens
[PASS] coluna 'data_cadastro' existe em wms_ordens
[PASS] coluna 'data_atualizacao' existe em wms_ordens
[PASS] coluna 'usuario_cadastro' existe em wms_ordens
[PASS] coluna 'usuario_atualizacao' existe em wms_ordens
[PASS] índice único uniq_wms_ordens_ref existe (dedup AC6.2)
[PASS] índice idx_wms_ordens_status existe
[PASS] índice idx_wms_ordens_missao existe
[PASS] uniq_wms_ordens_ref é composto por (block_empresa_id, external_ref) — dedup é por empresa, não global
[PASS] FK wms_ordens.missao_logistica_id → ON DELETE SET NULL (confdeltype='n')
[PASS] Migration 002 rodada 2x seguidas sem erro (idempotente)

--- Migration 003: seed canal wms-7 + config entrada ---
[PASS] Migration 003 lida do disco
[INFO] Migration 003 aplicada (idempotente).
[PASS] canal 'wms-7' existe em webhook_endpoints
[PASS] wms-7: target_lab = ReceberOrdemLaboratorio
[PASS] wms-7: payload_mapper = WmsPayloadNormalizer
[PASS] wms-7: active = 1
[PASS] wms-7: customer_id = 7
[PASS] wms-7: block_empresa_id = 7
[PASS] wms-7: signature_header = X-Signature
[PASS] wms-7: signature_algo = hmac_sha256
[PASS] wms-7: secret não vazio
[PASS] integracao_wms_config da empresa 7 existe
[PASS] empresa 7: ativo_entrada = 1 (ligado pela seed 003)
[PASS] empresa 7: ativo_saida = 1 (push legado mantido)
[PASS] Migration 003 rodada 2x seguidas → ainda exatamente 1 canal "wms-7" (sem duplicar)
[PASS] Migration 003 rodada 2x seguidas → ainda exatamente 1 config para empresa 7

--- Caracterização: coluna 'block' pós-seed (ver ReceberOrdemApiTest.php p/ impacto) ---
[NOTE] webhook_endpoints.block para o canal "wms-7" = NULL
[NOTE] Ver tests/integration/IntegracaoWms/ReceberOrdemApiTest.php — bug de roteamento documentado ali.

====================================================
RESULTADO: PASS=41  FAIL=0
TODOS OS TESTES PASSARAM
```

### Integration — `ReceberOrdemApiTest.php` (bug corrigido — re-executado após o fix)
```
====================================================
INTEGRATION — IntegracaoWms Fase 2: receiver ponta-a-ponta (dev_gx_goldie)
====================================================

--- GRUPO A: canal seedado 'wms-7' deve estar imediatamente alcançável ---
[PASS] pré-condição: webhook_endpoints.block do canal 'wms-7' está populado pela Migration 003
[NOTE] POST /api/webhooks/wms-7 (assinatura HMAC VÁLIDA, canal seedado pela Migration 003):
[NOTE]   → HTTP 422 | body={"received":true,"channel":"wms-7","request_id":"test-a5e8d90b475d","ok":false,"ordem_id":99,"status":"rejeitada","motivo":"Tipo não suportado nesta versão: Tipo de ordem sem tradutor implementado nesta versão: alocacao"}
[PASS] GRUPO A: POST assinado corretamente para o canal seedado NÃO deveria dar 404 (canal existe na tabela)

--- GRUPO B: fluxo normal do receiver ---

-- B1. version='v99' (desconhecida) --
[PASS] B1: version='v99' → HTTP 422
[PASS] B1: corpo rico → ok=false
[PASS] B1: corpo rico → status="rejeitada"
[PASS] B1: corpo rico → motivo menciona a versão recebida
[PASS] B1: corpo mantém "received"=true (compat com o formato antigo)
[PASS] B1: corpo mantém "channel"
[PASS] B1: NADA gravado em wms_ordens (tenant nem foi resolvido nesse ponto)

-- B2. Envelope válido, tipo='alocacao' (sem tradutor na Fase 2) --
[PASS] B2: HTTP 422 (ok=false → status 422 pelo fix do InboundHandler)
[PASS] B2: corpo rico → ok=false
[PASS] B2: motivo menciona falta de tradutor (Fase 2)
[PASS] B2: ordem_id retornado no corpo
[PASS] B2: linha gravada em wms_ordens
[PASS] B2: linha gravada com status="rejeitada"
[PASS] B2: linha gravada na empresa correta (7, resolvida pelo canal wms-7)

-- B3. Reenvio do MESMO external_ref (dedup) --
[PASS] B3: reenvio → HTTP 422 (a rejeição original é preservada, não é sucesso)
[PASS] B3: reenvio → status="ja_processada" no corpo
[PASS] B3: reenvio NÃO duplica linha em wms_ordens (ainda 1 só)

-- B4. Assinatura HMAC inválida --
[PASS] B4: assinatura HMAC inválida → HTTP 401
[PASS] B4: corpo → error.code = "hmac_mismatch"
[PASS] B4: assinatura inválida → ReceberOrdemLaboratorio NUNCA executa (nada gravado)

-- B5. Canal inexistente (nunca registrado) --
[PASS] B5: canal nunca registrado → HTTP 404 (comportamento correto — "mantém verde" do techplan)
[PASS] B5: corpo → error.code = "unknown_channel"

--- GRUPO C: cross-tenant via HTTP (2 canais, mesmo external_ref) ---
[PASS] cross-tenant: POST em wms-7 processado normalmente (422 — sem tradutor)
[PASS] cross-tenant: POST em wms-99941 processado normalmente (422 — sem tradutor)
[PASS] cross-tenant: linha da empresa 7 existe (mesmo external_ref da empresa X)
[PASS] cross-tenant: linha da empresa de teste existe
[PASS] cross-tenant: tipo da empresa 7 é o que ELA enviou ("movimentacao")
[PASS] cross-tenant: tipo da empresa X é o que ELA enviou ("inventario"), sem se confundir com a 7
[PASS] cross-tenant: exatamente 2 linhas para o mesmo external_ref (uma por empresa, sem colisão)

[CLEANUP] Linhas de teste removidas. webhook_endpoints.block('wms-7') permanece populado (estado correto pós-fix).

====================================================
RESULTADO: PASS=31  FAIL=0  (dos quais BUGS=0)
SUÍTE CONCLUÍDA (bugs conhecidos documentados acima, se houver).
```

### Baseline Fase 1 (não-regressão)
```
GalpaoProviderParidadeTddTest.php  → RESULTADO: PASS=35  FAIL=0  TODOS OS TESTES PASSARAM
WmsProviderRegistryTddTest.php     → RESULTADO: PASS=27  FAIL=0  TODOS OS TESTES PASSARAM
IntegracaoWmsConfigSondaTddTest.php→ RESULTADO: PASS=21  FAIL=0  TODOS OS TESTES PASSARAM
AlocacaoHookStrategyTddTest.php    → RESULTADO: PASS=41  FAIL=0  TODOS OS TESTES PASSARAM
ConfigIntegracaoTest.php           → RESULTADO: PASS=47  FAIL=0  TODOS OS TESTES PASSARAM
```

## Bugs encontrados (e corrigidos)

**Status: ✅ CORRIGIDO pelo orquestrador logo após esta rodada de testes** — ver
`Migrations/003_seed_webhook_endpoint_dev.sql` (INSERT agora popula `block`; mais
um `UPDATE ... WHERE block IS NULL` idempotente pra reparar ambientes já seedados
antes do fix) e `InboundHandler.php` (inalterado nesta correção). O teste
`ReceberOrdemApiTest.php` foi atualizado — o Grupo A deixou de forçar/reproduzir
o bug e agora é um guard de não-regressão (`expectFixed`) que falharia caso o bug
volte. Suíte inteira re-executada: 400/400 passing, 0 bugs.

1. **[CRÍTICO, CORRIGIDO] O canal `wms-7` seedado pela `Migration 003` era inalcançável via HTTP —
   `POST /api/webhooks/wms-7` sempre responde `404 unknown_channel`, mesmo com
   assinatura HMAC correta.**
   - **Arquivo/linha**: `componentes/Missoes/Logistica/sinais/IntegracaoWms/Migrations/003_seed_webhook_endpoint_dev.sql:23-38` (o `INSERT INTO webhook_endpoints` popula `customer_id` e `block_empresa_id`, mas **não** a coluna `block`).
   - **Causa raiz**: `Api\Sondas\WebhookEndpointSonda` tem `customerControl=true`
     (`componentes/Missoes/Plataforma/sinais/Api/Sondas/WebhookEndpointSonda.php:38-45`).
     O `find()` do `ModelInteligencia` (V2) injeta automaticamente
     `(block = :bcustomerid OR block = :bcustomerid2)` em qualquer query quando
     `customerControl` está ligado (`controladores/_Controladores/ModelInteligencia.php:1289-1309`),
     e `:bcustomerid2` é uma constante fixa (`7`, "customer mestre" do framework).
     Como a linha seedada tem `block = NULL`, a condição `NULL = 7` nunca é
     verdadeira em SQL — `InboundHandler::handle()` → `WebhookEndpointSonda::findByChannel('wms-7')`
     (`componentes/Missoes/Plataforma/sinais/Api/Handlers/Webhooks/InboundHandler.php:31-33`)
     **sempre** retorna `null`, **independente da sessão, do secret ou de qualquer
     coisa que o chamador faça** — não é uma nuance de tenant, é uma coluna vazia.
   - **Impacto**: o receiver de entrada inteiro da Fase 2 (`ReceberOrdemLaboratorio`,
     `WmsPayloadNormalizer`, dedup, validação de identidade) **nunca é alcançado** por
     uma requisição HTTP real, mesmo estando 100% funcional internamente (confirmado
     pelos testes que chamam o Laboratorio diretamente). O canal existe na tabela,
     está `active=1`, mas o roteamento genérico da Plataforma não o encontra.
   - **Reproduzido por**: `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php`
     ("GRUPO A") — determinístico, roda em qualquer ambiente `dev_gx_goldie` que
     tenha só a Migration 003 aplicada (sem nenhum patch manual).
   - **Fix sugerido** (não aplicado — não é escopo do Tester consertar código de
     produção): adicionar ao final da Migration 003 um
     `UPDATE webhook_endpoints SET block = block_empresa_id WHERE channel = 'wms-7' AND block IS NULL;`
     — ou popular `block` diretamente no `INSERT` (`..., block_empresa_id, block) VALUES (..., 7, 7)`).
     Validei manualmente que, com `block` populado, `findByChannel()` funciona e toda
     a cadeia downstream passa (ver Grupo B do teste, que aplica esse UPDATE **apenas
     no escopo do teste** e reverte no `finally`).
   - **Reporte**: encaminhar ao dev backend responsável pela Fase 2 (autor de
     `07-backend-fase2.md`) para corrigir a Migration 003. Este bug **não é
     mencionado** no `07-backend-fase2.md` — é um achado novo desta rodada de
     testes (o backend documentou um problema de tenant relacionado, item 2 de
     "Desvios do techplan", mas descreveu como "pode não encontrar a linha certa se
     a sessão não bater", não como "nunca encontra em NENHUMA circunstância" — a
     causa raiz real é mais grave que o originalmente suspeitado: não é sobre sessão,
     é sobre a coluna `block` nunca ter sido populada pelo INSERT cru).

## Observações (não são bugs — comportamento correto, só documentando nuances)

1. **Texto exato do motivo de rejeição por falta de tradutor difere do texto
   citado no docblock/"código morto" de `ReceberOrdemLaboratorio`.** O motivo
   realmente emitido e persistido é
   `"Tipo não suportado nesta versão: Tipo de ordem sem tradutor implementado nesta versão: {tipo}"`
   (concatenação do prefixo do `catch` com a mensagem da exceção do Factory —
   `Laboratorios/ReceberOrdemLaboratorio.php:120-132`), e **não** literalmente
   `"Tradutor não implementado nesta versão."` como aparece no bloco de código
   **morto/inalcançável** logo abaixo (`:134-146`, documentado como tal no próprio
   comentário: "Inalcançável enquanto o mapa do Factory estiver vazio"). Isso não é
   um bug funcional — a rejeição acontece corretamente e o motivo é informativo — só
   uma discrepância cosmética entre o comentário do bloco morto e o texto real
   observável. Meus testes (`ReceberOrdemLaboratorioTddTest.php`, `ReceberOrdemApiTest.php`)
   verificam o texto REAL (`str_contains` por trechos estáveis: "Tipo não suportado
   nesta versão" e "sem tradutor implementado"), não o texto do bloco morto.
2. **Achado do próprio backend confirmado e ainda mais grave na prática**: o
   `07-backend-fase2.md` (item 2) já alertava que `WebhookEndpointSonda` roda sob
   tenant "default" antes de qualquer sessão real existir, e recomendava ao Tester
   validar isso especificamente. A investigação desta rodada refinou o diagnóstico:
   o problema não é "sessão errada" — é que **nenhuma sessão jamais resolveria** a
   busca, porque a coluna `block` da linha seedada é `NULL` (ver bug #1 acima).
3. **`bcustomerid2` fixo em `7`** (constante interna do `ModelInteligencia`,
   `controladores/_Controladores/ModelInteligencia.php:1305`) faz qualquer linha
   com `block=7` ficar visível independente da sessão ativa — usado deliberadamente
   nos testes desta rodada (`empresa 7` como tenant de teste padrão) para reduzir
   fragilidade de setup, já que é o mesmo tenant usado pela seed de dev da Fase 1/2.

## Rastreabilidade dos ACs do techplan cobertos

| AC | Cobertura |
|---|---|
| AC3.3 / AC6.3 (resposta inequívoca ao WMS: aceite/rejeição observável) | `ReceberOrdemApiTest.php` (Grupo B) — corpo HTTP rico validado após o fix do `InboundHandler` |
| AC3.4 (2 tenants, sem vazamento) | `WmsOrdemSondaTddTest.php` (isolamento) + `ReceberOrdemApiTest.php` (Grupo C, cross-tenant via HTTP) |
| AC3.5 (versão de contrato desconhecida rejeitada) | `WmsPayloadNormalizerTddTest.php` + `ReceberOrdemApiTest.php` (B1) |
| AC4.4 (tipo sem tradutor rejeitado, nunca missão parcial) | `TraducaoOrdemFactoryTddTest.php` + `ReceberOrdemLaboratorioTddTest.php` (passo 6) + `ReceberOrdemApiTest.php` (B2) |
| AC5.1-5.4 (identidade desconhecida rejeitada com motivo) | `OrdemIdentidadeValidatorTddTest.php` + `ReceberOrdemLaboratorioTddTest.php` (passo 4) |
| AC6.1/6.2 (dedup, reenvio não duplica) | `WmsOrdemSondaTddTest.php` (UNIQUE) + `ReceberOrdemLaboratorioTddTest.php` (passo 7) + `ReceberOrdemApiTest.php` (B3) |
| AC1.4 (defesa em profundidade `ativo_entrada`) | `ReceberOrdemLaboratorioTddTest.php` (passo 2) |
| AC2.4/7.2/7.4 (canais independentes, nada se perde) | `ReportarOrdemLaboratorioTddTest.php` (passos 4, 5, 5b) |
| AC8.2 (origem WMS distinguível via `referencia_tipo`) | `ReportarOrdemLaboratorioTddTest.php` (passo 1, missão nativa vs `WMS_ORDEM`) |
| "Mantém verde": receiver opt-in, canal inexistente → 404 como hoje | `ReceberOrdemApiTest.php` (B5) |
