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

Escopo: Receiver de entrada + tradução (scaffold) + idempotência + origem +
ciclo de report. Passos 8-15 de `02-techplan.md`. Nenhum tradutor concreto
(Fase 3) foi implementado — comportamento correto e esperado desta fase.

## Arquivos criados

- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Migrations/002_create_wms_ordens.sql` — cria `wms_ordens` (idempotência por `UNIQUE(block_empresa_id, external_ref)`, ciclo de status, `payload_bruto` JSONB); inclui `usuario_cadastro` (lição do B2 da Fase 1).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Migrations/003_seed_webhook_endpoint_dev.sql` — registra o canal `wms-7` em `webhook_endpoints` (target_lab=ReceberOrdemLaboratorio, payload_mapper=WmsPayloadNormalizer) + liga `ativo_entrada=1` em `integracao_wms_config` para a empresa 7 (dev_gx_goldie).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Sondas/WmsOrdemSonda.php` — Sonda de `wms_ordens` (dedup por external_ref, criar/atualizarStatus, `customerControl=false`/isolamento explícito por `block_empresa_id`, espelha `IntegracaoWmsConfigSonda`).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Tradutores/OrdemTipo.php` — enum fechado dos 4 tipos de ordem (separacao|movimentacao|alocacao|inventario).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Tradutores/TraducaoOrdemInterface.php` — contrato que os `*Tradutor` concretos da Fase 3 implementarão.
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Tradutores/TraducaoOrdemFactory.php` — Factory+enum espelhando `OperacaoAdapterFactory`; **mapa vazio de propósito** — `create()` sempre lança `InvalidArgumentException` nesta fase.
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Webhooks/WmsPayloadNormalizer.php` — `implements Api\Webhooks\PayloadNormalizer`; resolve/valida `version` do envelope (só `v1` suportada); versão desconhecida marca o payload com `_envelope_invalido`/`_motivo` (não lança — ver "Desvios do techplan").
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Validators/OrdemEnvelopeValidator.php` — campos comuns obrigatórios (`external_ref`, `tipo`; `itens` opcional).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Validators/OrdemIdentidadeValidator.php` — resolve produto (`importacao_id`) e endereço (`wms_localizacoes.codigo` via `WmsLocalizacaoSonda::buscarPorCodigo`) dos itens da ordem; retorna motivo determinístico quando desconhecido (AC5.1-5.4).
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Laboratorios/ReceberOrdemLaboratorio.php` — `target_lab` do canal: resolve tenant do canal autenticado, estabelece sessão sintética, roda Envelope→Dedup→Identidade→Tradução(Factory vazio ⇒ rejeita com motivo), persiste `wms_ordens`.
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Laboratorios/ReportarOrdemLaboratorio.php` — fecha o ciclo report-back: avança `wms_ordens.status → 'reportada'` quando a missão é `referencia_tipo='WMS_ORDEM'` e há pelo menos 1 movimento com `integracao_erp|integracao_wms = 'enviado'`. É um no-op seguro nesta fase (nenhuma missão nasce com essa referência ainda).

## Arquivos alterados

- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Provedores/Http/HttpWmsProvider.php` — saiu do stub: agora faz POST JSON real (cURL) com `external_ref` no corpo e assinatura HMAC out (`X-Signature: sha256=...`), reaproveitando o `bearerToken` já resolvido pela strategy como segredo de assinatura (override opcional via `define('GENESIS_WMS_HTTP_HMAC_SECRET', ...)`).
- `crons/wms_alocacao_integracao.php` — adicionado bloco isolado (ver seção dedicada abaixo) que chama `ReportarOrdemLaboratorio` depois de cada tentativa de push (ERP+WMS) por missão.

Não alterados (conforme escopo): `IntegracaoWms.php`, `Provedores/{WmsProviderRegistry,WmsProviderFactory,WmsProviderInterface,WmsEnvioContexto,WmsEnvioResultado,Galpao/GalpaoProvider}.php`, `Config/*`, `Api/Bootstrap.php` (nada novo a registrar em código — o canal é 100% dado, via `webhook_endpoints`), `Api/Routes.php`, `Api/Handlers/Webhooks/InboundHandler.php`, `Api/Sondas/WebhookEndpointSonda.php`, `Api/Webhooks/{HmacValidator,PayloadNormalizer}.php`.

## Desvios do techplan / decisões técnicas onde o plano deixou espaço

Nenhum desvio do *conteúdo* do techplan — os passos 8-15 foram seguidos à
risca. Os itens abaixo são **decisões de implementação** que o techplan
deixou em aberto (a letra dele não especifica o mecanismo exato), mais **um
gap descoberto** na infra compartilhada que não é meu para consertar.

### 1. Gap descoberto no `InboundHandler` (infra compartilhada, fora do meu escopo)
`InboundHandler::handle()` (não alterado por mim) calcula
`$resposta = property_exists($lab, 'response') ? (array) $lab->response : [];`
mas **nunca usa essa variável** — a resposta HTTP final ao WMS é sempre
`200 {received:true, channel, request_id}` (ou um 4xx genérico só se algo
lançar uma exceção não capturada). Ou seja: o veredito rico que
`ReceberOrdemLaboratorio->response` produz (`ok/ordem_id/status/motivo`) **não
chega ao WMS pelo corpo da resposta HTTP** hoje — só fica persistido em
`wms_ordens` (observável via banco, o que cobre AC6.3, mas não cobre
literalmente "resposta ao WMS... rejeição (com motivo)" de AC3.3/AC3.5 no
nível de transporte).
Não toquei em `InboundHandler.php` (fora do meu escopo: é infra genérica da
Plataforma, não do sinal `IntegracaoWms`, e a instrução era usá-la como está).
**Sugestão de follow-up (não aplicada por mim):** trocar a última linha de
`InboundHandler::handle()` por
`$res->json(array_merge(['received' => true, 'channel' => $channel, 'request_id' => $req->requestId], $resposta), $resposta['ok'] ?? true ? 200 : 422);`
— mudança de 1 linha, mas afeta qualquer outro canal futuro que já dependa do
formato atual, por isso não a apliquei sem validação do Tech Lead/orquestrador.

### 2. Resolução de tenant a partir do canal (channel não chega ao target_lab)
O `InboundHandler` instancia o Lab como `new $targetLab($payload)` — só o
payload já normalizado chega, **não** o `$channel` nem o
`customer_id`/`block_empresa_id` da linha de `webhook_endpoints`. Para
`ReceberOrdemLaboratorio` "resolver o tenant do canal autenticado" (linguagem
literal do techplan) sem alterar o `InboundHandler`, adotei a convenção de
nomear o canal como `wms-{empresaId}` e fiz `ReceberOrdemLaboratorio` extrair
o `empresaId` **da própria URL da requisição** (`$_SERVER['REQUEST_URI']`),
com um regex restrito a `^wms-(\d+)$`. Isso não é um valor arbitrário: só é
alcançável depois que o `HmacValidator` já validou a requisição contra o
`secret` daquele canal específico (a mesma garantia de autenticidade que o
`InboundHandler` já dá). Documentado nos docblocks de
`ReceberOrdemLaboratorio` e da migration 003.

Como consequência, também descobri que `WebhookEndpointSonda` tem
`customerControl=true`/`companyControl=true`, e `findByChannel()` (chamado
pelo `InboundHandler` **antes** de qualquer sessão de tenant existir) aplica
esses filtros usando sessão que ainda não foi estabelecida — na prática, o
lookup do canal roda sob o tenant "default" do framework (fallback
hard-coded), não sob o tenant do canal em si. Isso é uma característica da
infra genérica (fora do meu escopo consertar), mas registro aqui porque é
relevante para o Tester: **o dedup/lookup do próprio canal em
`webhook_endpoints` pode não encontrar a linha certa se a sessão "default" do
ambiente não bater com a empresa seedada**; recomendo ao Tester validar isso
especificamente no ambiente `dev_gx_goldie` antes de assumir que qualquer
empresa além da default funciona ponta-a-ponta.

### 3. `ativo_entrada` como defesa em profundidade
O techplan não deixa explícito se `ReceberOrdemLaboratorio` deve checar
`integracao_wms_config.ativo_entrada` (a config já existe desde a Fase 1,
opt-in, default 0) além da própria existência do canal HMAC. Adicionei essa
checagem (rejeita com motivo claro se `ativo_entrada=0`) por dois motivos:
espelha o padrão de defesa-em-profundidade já usado na Fase 1 (M2 do review:
guard de backend + UI para `http_wms`), e a coluna já existe sem nenhum lugar
que a leia — deixá-la morta seria pior. Por isso a migration 003 também liga
`ativo_entrada=1` para a empresa de dev seedada (sem isso, o canal existiria
mas o Lab rejeitaria toda ordem).

### 4. Contrato v1 do envelope/itens (assumido, não especificado no techplan)
O techplan não fixa o schema exato do envelope. Assumi, documentando nos
docblocks de `WmsPayloadNormalizer` e dos Validators:
```json
{ "version": "v1", "external_ref": "...", "tipo": "separacao|movimentacao|alocacao|inventario",
  "itens": [ { "produto_ref": "...", "endereco_ref": "...", "quantidade": n } ], "prioridade": 0 }
```
`produto_ref`/`importacao_id` e `endereco_ref`/`localizacao_codigo` são
aceitos como aliases. Isso é scaffold — a Fase 3 pode ajustar por tipo de
ordem sem quebrar `OrdemEnvelopeValidator` (só valida `external_ref`/`tipo`).

### 5. Dedup retorna o status real da ordem, não sempre "aceita"
Ao reenviar um `external_ref` já visto, respondo `status: 'ja_processada'`
com `ok = ($existente->status !== 'rejeitada')` e o `motivo` originalmente
gravado — em vez de sempre devolver um aceite genérico. Julguei isso mais
correto para AC6.3 ("observável") do que mascarar uma rejeição anterior como
sucesso.

### 6. `ReportarOrdemLaboratorio`: gatilho de "push enviado"
O techplan diz "depois que o push для enviado, chamar ReportarOrdemLaboratorio".
As duas `HookStrategy` (ERP/WMS) não retornam sucesso/falha ao cron (void).
Decidi: `ReportarOrdemLaboratorio::reportar($missaoId)` é chamado
incondicionalmente pelo cron após cada tentativa de push (idempotente e
barato), e é o **próprio Laboratorio** quem confere via
`WmsMovimentoSonda::listarPorMissao()` se **pelo menos um** movimento da
missão já confirmou `EstadoIntegracao::ENVIADO` em qualquer um dos dois
canais. Se ainda não, retorna `false` silenciosamente e o próximo ciclo do
cron tenta de novo (AC7.2/7.4 — nada se perde). Não exigi que **ambos** os
canais estejam `enviado`, pois o techplan trata os dois estados como
independentes (AC2.4).

### 7. `HttpWmsProvider`: segredo de assinatura HMAC out
Não posso alterar `IntegracaoWmsConfigSonda`/`IntegracaoWmsEnvConfig` (Fase 1
committed, fora do escopo desta tarefa). Como não há hoje nenhum campo
dedicado a "segredo de assinatura HMAC de saída" resolvido para valor real
(`token_saida_ref` é só referência/UI, sem resolução para env desde a Fase
1), reusei o `bearerToken` já resolvido pela strategy chamadora
(`IntegracaoWmsEnvConfig::bearerToken()`) como segredo de assinatura,
com um override dedicado via `define('GENESIS_WMS_HTTP_HMAC_SECRET', ...)`
quando um segredo diferente for necessário no futuro — sem exigir mudança na
Fase 1.

## Isolamento da mudança em `crons/wms_alocacao_integracao.php`

O arquivo já tinha alterações não commitadas de outra feature (aprovação de
gestor + fila de estorno) quando comecei. Minha mudança está concentrada em
**dois pontos, ambos marcados com comentários explícitos**:

1. Um bloco único e contíguo logo após os `use` do topo do arquivo:
   ```php
   use GalaxiaMissoes\Logistica\sinais\IntegracaoWms\Laboratorios\ReportarOrdemLaboratorio;

   // =====================================================================
   // Integração WMS desacoplada (Fase 2) — bloco ISOLADO, não mexer na lógica
   // de aprovação/estorno acima (outra feature em andamento, não commitada).
   // ...
   function reportarOrdensWmsPorMissao(array $missaoIds): void { ... }
   // ==================== fim do bloco isolado (Fase 2) ====================
   ```
2. Uma única linha de chamada, com comentário de marcação, logo depois do
   `foreach ($porMissaoWms as $missaoId => $itens) { $hookWms->sincronizar(...); }`
   e ANTES do comentário `// --- Processar fila de ESTORNO ERP ...` (que
   pertence à outra feature):
   ```php
   // Integração WMS desacoplada (Fase 2) — ver bloco isolado no topo do arquivo.
   reportarOrdensWmsPorMissao(array_merge(array_keys($porMissaoErp ?? []), array_keys($porMissaoWms ?? [])));
   ```

Nenhuma linha da lógica de aprovação/estorno foi tocada ou reordenada. Como
minha 2ª inserção é fisicamente adjacente ao início do bloco de estorno (já
presente e não commitado), o `git diff`/`git add -p` pode agrupar os dois num
único hunk mecânico — mas as fronteiras são inequívocas pelos comentários
(`// Integração WMS desacoplada (Fase 2) — ver bloco isolado...` é a última
linha minha; a linha seguinte, `// --- Processar fila de ESTORNO ERP...`, já
é da outra feature). Recomendo `git add -p` com edição manual do hunk (`e`)
nesse ponto específico, usando esses comentários como âncora, caso precise
separar cirurgicamente os dois commits.

## Comandos de verificação executados

- `php -l` em todo PHP novo/alterado desta fase → **OK** em todos:
  - `Sondas/WmsOrdemSonda.php`
  - `Tradutores/OrdemTipo.php`
  - `Tradutores/TraducaoOrdemInterface.php`
  - `Tradutores/TraducaoOrdemFactory.php`
  - `Webhooks/WmsPayloadNormalizer.php`
  - `Validators/OrdemEnvelopeValidator.php`
  - `Validators/OrdemIdentidadeValidator.php`
  - `Laboratorios/ReceberOrdemLaboratorio.php`
  - `Laboratorios/ReportarOrdemLaboratorio.php`
  - `Provedores/Http/HttpWmsProvider.php`
  - `crons/wms_alocacao_integracao.php`
- Smoke script próprio (`autoload.php` real, sem tocar `dev_gx_goldie` — banco
  remoto não alcançável deste ambiente, então **não** é um teste de
  integração): confirmou que todas as classes carregam, `HttpWmsProvider`
  implementa `WmsProviderInterface`, `WmsPayloadNormalizer` implementa
  `Api\Webhooks\PayloadNormalizer`, `TraducaoOrdemFactory::create()` lança
  para os 4 `OrdemTipo` (comportamento esperado), o parser de versão do
  envelope rejeita `v99` e aceita `v1`/default, `OrdemEnvelopeValidator`
  valida campos obrigatórios, e o parsing de canal
  (`wms-7` → `empresaId=7`) funciona. Rodei também o fluxo completo de
  `ReceberOrdemLaboratorio` com `REQUEST_URI=/api/webhooks/wms-7`: sem banco
  real disponível, `IntegracaoWmsConfigSonda::obterConfig()` caiu no guard de
  deploy (try/catch → `configDefault()`), e o Lab corretamente rejeitou com
  `"Integração de entrada desativada para esta empresa."` — nenhum fatal
  error em toda a cadeia (autoload → sessão sintética → config → validators →
  dedupe → factory).
- Não rodei testes de integração (Tester) nem escrevi TddTests exaustivos,
  conforme instrução.

## Próximos passos para o frontend

Nenhum novo endpoint de UI foi criado nesta fase — o receiver é
`/api/webhooks/wms-{empresaId}` (superfície externa, não `/api/v1`, sem
consumo por frontend Genesis). A tela de config (`html/integracaowms.php`,
Fase 1) já expõe `ativo_entrada`/`contrato_versao` — nenhuma mudança de UI é
necessária para a Fase 2. Quando a Fase 3 plugar os tradutores concretos, a
missão criada será visível no app do operador exatamente como qualquer
missão nativa (mesma UX, só `referencia_tipo='WMS_ORDEM'` a distingue nos
dados, não na tela).

Se o frontend precisar inspecionar o ciclo de uma ordem WMS (ex.: uma tela de
observabilidade futura), a fonte de dados é a tabela `wms_ordens`
(`status`, `motivo`, `missao_logistica_id`, `recebida_em`, `reportada_em`) —
não há Handler de `/api/v1` para isso ainda; não estava no escopo desta fase
e não foi pedido pelo techplan.
