# Review — Movimentação WMS (operacao_tipo=movimentacao)

**Base**: `dev_team` @ `3d7a12de` (working tree — feature ainda não commitada)
**HEAD**: working tree (uncommitted)
**Arquivos tocados**: 6 novos + 7 alterados (adapter, enum, factory, sonda, laboratorio, dispatcher, cron, view, js, testes, i18n)
**Linhas (novos)**: ~2.529 (migration 31, adapter 166, view 977, js 798, testes ~830)

---

## Ciclo 2 — Re-revisão pós-fixes (2026-06-24) — GATE FINAL

### Veredito

✅ **APROVADO — 0 blockers, 0 majors.** Os três fixes do ciclo 2 foram aplicados e
verificados no código-fonte. O único major que restava (M2 — lista exibindo "Produto #ID"
em vez do nome) foi resolvido pelo caminho **preferido** que a re-revisão recomendou:
enriquecimento em batch a partir do snapshot `missao_logistica_itens.dados_adicionais`,
sem N+1 e sem raw SQL, espelhando `ListarAlocacoesLaboratorio`. O gap de cobertura que
escondeu o B1 original também foi fechado (seção 7 do E2E assere o retorno de
`listarMovimentos`). Os minors m1 e nit n2 entraram junto. Feature pronta para merge.

### Re-verificação dos fixes do ciclo 2

#### ✅ M2 — RESOLVIDO (enriquecimento de `produto_nome` em batch)
**Onde**: `AlocacaoWms.php:234-326` (`listarMovimentos` + novo `resolverNomesProdutos`).
`listarMovimentos` agora monta as linhas com `produto_nome => null` (`:253`) e, num segundo
passo (`:266-273`), coleta os `produto_id` únicos (`array_column` + `array_unique`) e popula
via `resolverNomesProdutos($missaoId, $produtoIds)` num único passe por referência. O método
novo (`:290-326`) carrega **todos** os itens da missão em uma query
(`MissaoLogisticaItem::findAny('missao_logistica_id = :m', ..., all=true)`, `:296-302`),
normaliza o retorno com o mesmo `is_array(...) ? : ($x ? [$x] : [])` (`:303`) e lê o nome de
`dados_adicionais['produto_nome']` com fallback para a coluna top-level (`:311-314`) — **idêntico**
ao padrão validado em `ListarAlocacoesLaboratorio.php:61-66`. Garante uma entrada por
`produto_id` pedido (`:319-323`), retornando `null` gracioso se o item não existir. **Zero N+1,
zero raw SQL** (memória `feedback_no_raw_sql` respeitada — usa o model, não `Connect`). O acesso
`$item->produto_id`/`->dados_adicionais`/`->produto_nome` funciona via o `__get` da V1
ModelInteligencia (`$this->data->$name`, memória `model_inteligencia_data_get`), consistente com
o caminho de referência. A `WmsMovimentoSonda::listarPorMissao` (`:161-169`) devolve `SELECT *`
ordenado por `confirmada_em DESC`, então todas as colunas serializadas (`origem`/`destino`/
`quantidade`/`operacao_tipo`/`confirmada_em`) chegam não-null. **Sintoma do M2 ("Produto #ID")
eliminado**: o JS (`movimentacao.js`) agora recebe `produto_nome` populado e nunca cai no fallback.

#### ✅ Cobertura M2 — gap fechado (seção 7 do E2E)
**Onde**: `MovimentacaoE2ETddTest.php:270-290`. O teste invoca o método privado real
`listarMovimentos` via Reflection (`:275-280`) contra a missão temporária cujos itens foram
criados pelo **caminho de produção** (`ConfirmarAlocacaoLaboratorio::executar` semeou os
`missao_logistica_itens` com `dados_adicionais.produto_nome` — confirmado pelo `item_id` em
`:124-125` e o COUNT em `:251`). Asserções: `ok===true` (`:282`), `movimentos` não-vazio (`:283`),
`produto_nome` presente (`:285`), **não-null** (`:286`), **string** (`:287`) e **nenhum**
movimento sem nome (`:289-290`). São 6 asserções novas reais (não-stub) — exatamente o tipo de
verificação de contrato cuja ausência permitiu o B1 do ciclo 1 passar. Gap fechado.

#### ✅ m1 — RESOLVIDO (`$item` morto no cron)
**Onde**: `wms_alocacao_integracao.php:137` e `:166`. Os dois `foreach ($itens... as $i => $item)`
viraram `foreach (array_keys($itensErp) as $i)` / `foreach (array_keys($itensWms) as $i)`, e os
corpos indexam `$pendentesErp[$i]`/`$itensErp[$i]` (`:138-139`) e `$pendentesWms[$i]`/`$itensWms[$i]`
(`:167-168`). Como `$itensErp`/`$itensWms` são `array_map` sobre `$pendentes*`, compartilham as
chaves — funcionalmente equivalente e sem variável morta.

#### ✅ n2 — RESOLVIDO (`"Total"` hardcoded)
**Onde**: `movimentacao.php:772` agora `<div class="app-stat-rotulo"><?= show('Total') ?></div>`,
pareando com a vizinha `show('Concluídos')` (`:777`). Toda a view usa `show("PT")` md5 (memória
`galaxia_i18n_show_md5`); a chave já estava harvestada nos 3 idiomas.

### Status dos demais findings do ciclo 1 (sem mudança)

| # | Item | Status |
|---|------|--------|
| B1 | Lista não exibia movimentos (contrato `listar` ≠ JS) | ✅ Resolvido (ciclo 1, endpoint `listarMovimentos`) |
| M1 | Extrato gravava `localizacao_codigo` vazio | ✅ Resolvido (ciclo 1) |
| m2 | Redundância `$locId`/`$locDestinoId` | 🟡 Parcial (comentário explicativo) — aceitável, não-bloqueante |
| m3 | origem≠destino sem normalização de caixa | ⚪ Fora de escopo (limitação compartilhada por todas as operações) |
| n1 | Comentário enganoso `movimentacao.js` | ✅ Resolvido (ciclo 1) |

### Verificação técnica (ciclo 2)

- `php -l` limpo nos 4 arquivos do ciclo 2: `AlocacaoWms.php`, `wms_alocacao_integracao.php`,
  `movimentacao.php`, `MovimentacaoE2ETddTest.php`.
- Unit tests verdes (sem regressão): `OperacaoTipoTddTest` **23/23**, `MovimentacaoAdapterTddTest` **40/40**.
- E2E (`MovimentacaoE2ETddTest`) não roda neste sandbox — exige `dev_gx_goldie` (memória
  `feedback_integration_tests`); asserções estaticamente sólidas e lint-clean.

### Nit residual (não-bloqueante, registro)

#### n3 — Numeração de seção do E2E fora de ordem (cosmético)
**Onde**: `MovimentacaoE2ETddTest.php:262` (`---- 7. ...`), `:270` (`---- 8. M2 ...`) mas o `echo`
em `:273` imprime `--- 7. M2 ...`. Comentário diz "8", saída diz "7" — duas seções "7" no log.
Sem qualquer impacto funcional ou de asserção; ajustar a label quando conveniente. Não trava merge.

### Pontos fortes (ciclo 2)

- **M2 corrigido pelo caminho certo**: reuso explícito do padrão `ListarAlocacoesLaboratorio`
  (snapshot em `dados_adicionais`, batch via `findAny(all=true)`) em vez de gambiarra ou raw SQL.
  Comentários (`:263-265`, `:279-285`) citam a fonte e o gotcha do `all=true` — autodocumentado.
- **Teste fecha o loop de regressão**: a seção 7 do E2E assere o contrato de saída do endpoint,
  não só o write em `wms_movimentos`. É a lição do B1 aplicada — agora um futuro refactor que
  quebre o enriquecimento de nome falha o teste.
- **Fixes cirúrgicos**: m1 e n2 são mudanças mínimas e isoladas, sem efeito colateral; unit suites
  permanecem 100% verdes, confirmando ausência de regressão.

### Recomendação final

✅ **MERGE.** 0 blockers, 0 majors. B1, M1 e M2 resolvidos; cobertura de regressão adicionada;
minors m1/n2 fechados. Resta apenas o nit cosmético n3 (label de seção no log do E2E) e o m2
parcial (comentado, aceitável) — nenhum trava o gate. A fundação (adapter sob `OperacaoAdapterBase`,
Sonda append-only, migration 005 aditiva com `block_*` sem `DEFAULT`, extrato rastreável,
i18n nos 3 idiomas, view↔js pareados, write path testado E2E) está sólida e segue as referências
V2 (Otp/Edicaorota). **Feature aprovada para merge.**

---

## Ciclo 1 — Review original (histórico)

**Recomendação do ciclo 1**: ⚠️ Voltar pro dev (1 blocker B1 + 1 major M1).

### B1 (ciclo 1) — Lista da sessão não exibia os movimentos (contrato `listar` ≠ o que o JS esperava)
`movimentacao.js` pedia `listar` (→ `ListarAlocacoesLaboratorio::listarItensDaMissao`, que devolve
itens de `missao_logistica_item`: sem origem/destino, qty=1, 1 por produto) mas o `_renderLista`
esperava linhas brutas de `wms_movimentos`. Resultado: "— → — qty 1" por produto. Violava AC3.2/AC2.5.
→ **Resolvido no ciclo 1** via endpoint `listarMovimentos`.

### M1 (ciclo 1) — Extrato de auditoria gravava `localizacao_codigo` vazio
`registrarExtrato` usava `'localizacao_codigo' => $locCodigo`, e para movimentação `$locCodigo`
era `''` (o front manda `_origem`/`_destino`) → extrato perdia origem/destino.
→ **Resolvido no ciclo 1** (`$locCodigo ?: $locDestinoCodigo` + campos de rastreio).

### Minors/nits do ciclo 1
- m1: `$item` morto no cron → ❌ aberto (ver acima)
- m2: redundância `$locId`/`$locDestinoId` → 🟡 parcial (comentário adicionado)
- m3: comparação origem≠destino sem normalização de caixa (limitação compartilhada com todas as
  operações; fora de escopo) → registrado, não acionável aqui
- n1: comentário enganoso no `movimentacao.js` → ✅ resolvido
- n2: `"Total"` hardcoded → ❌ aberto (ver acima)
