# Implementação backend — Movimentação WMS

## Arquivos criados

- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Migrations/005_add_movimentacao_fields.sql` — migration aditiva e idempotente que adiciona `localizacao_codigo_origem` e `localizacao_codigo_destino` (VARCHAR 64, nullable) à `wms_movimentos`, mais dois índices parciais `idx_wms_movimentos_loc_origem` e `idx_wms_movimentos_loc_destino` (escopados por `block`, só para linhas não-NULL).
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Strategies/Operacao/MovimentacaoAdapter.php` — adapter da operação `movimentacao`; implementa validarOperacao (ordem produto→origem→destino→origem≠destino→qty>0→catálogo), montarMovimento (localizacao_codigo=destino, decisão A2), payloadPushErp/Wms (inclui origem+destino explícitos).
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/Operacao/MovimentacaoAdapterTddTest.php` — teste TDD CLI (40 assertions) cobrindo identidade, factory, push, validarOperacao (9 rejeições + 3 aceitações), montarMovimento e payloadPush*.

## Arquivos alterados

- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Enums/OperacaoTipo.php` — novo `case MOVIMENTACAO = 'movimentacao'`; `label()` ganhou o braço `self::MOVIMENTACAO => 'Movimentação'`; `exigeQuantidade()` inclui `self::MOVIMENTACAO => true`.
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Strategies/Operacao/OperacaoAdapterFactory.php` — registrado `'movimentacao' => MovimentacaoAdapter::class` no array `$adapters`.
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Sondas/WmsMovimentoSonda.php` — `registrarMovimento()` recebe dois parâmetros opcionais no fim (`?string $localizacaoOrigem = null`, `?string $localizacaoDestino = null`) e os grava como `localizacao_codigo_origem`/`localizacao_codigo_destino` no `$this->data`; chamadas existentes (sem esses args) deixam as colunas NULL.
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/ConfirmarAlocacaoLaboratorio.php` — extrai `localizacao_codigo_origem` e `localizacao_codigo_destino` de `$dados`; repassa esses campos ao `montarMovimento` (junto com `wms_localizacao_id_destino`); repassa `$mov['localizacao_codigo_origem']` e `$mov['localizacao_codigo_destino']` para os dois últimos args de `registrarMovimento`.
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/AlocacaoWms.php` — `confirmar()` inclui `localizacao_codigo_origem` e `localizacao_codigo_destino` de `$data` no `$dados` montado (default `''`; alocação/inventário não enviam esses campos e seguem sem mudança).
- `crons/wms_alocacao_integracao.php` — o `array_map` da fila ERP e o da fila WMS carregam `localizacao_codigo_origem`/`localizacao_codigo_destino` de `$aloc` (NULL para alocação/inventário; inofensivo, adapters dessas operações não leem os campos).
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Enums/OperacaoTipoTddTest.php` — atualizado: contagem de cases de 5 para 6; adicionadas asserções de `MOVIMENTACAO->value`, `fromConfig('movimentacao')`, `exigeQuantidade()===true`, `exigeEndereco()===true`, `exigeProduto()===true`.

## Desvios do techplan

Nenhum. Todas as mudanças seguem exatamente o §4, §5, §6, §7.1, §7.2, §7.4 e §8.1 do techplan.

## Comandos de verificação executados

- `php -l Enums/OperacaoTipo.php` → OK
- `php -l Sondas/WmsMovimentoSonda.php` → OK
- `php -l Strategies/Operacao/MovimentacaoAdapter.php` → OK
- `php -l Strategies/Operacao/OperacaoAdapterFactory.php` → OK
- `php -l Laboratorios/ConfirmarAlocacaoLaboratorio.php` → OK
- `php -l AlocacaoWms.php` → OK
- `php -l crons/wms_alocacao_integracao.php` → OK
- `php -l Testes/Strategies/Operacao/MovimentacaoAdapterTddTest.php` → OK
- Migration 005 aplicada em `dev_gx_goldie` → colunas e índices confirmados via query.
- `tests/integration/AlocacaoWms/MigrationSchemaTest.php` → PASS=41 FAIL=0
- `tests/integration/AlocacaoWms/AlocacaoFluxoTest.php` → PASS=29 FAIL=0 (regressão)
- `Testes/Strategies/Operacao/MovimentacaoAdapterTddTest.php` → PASS=40 FAIL=0
- `Testes/Strategies/Operacao/AlocacaoAdapterTddTest.php` → PASS=37 FAIL=0 (regressão)
- `Testes/Strategies/Operacao/InventarioProdutosAdapterTddTest.php` → PASS=28 FAIL=0 (regressão)
- `Testes/Strategies/Operacao/InventarioEnderecoAdapterTddTest.php` → PASS=19 FAIL=0 (regressão)
- `Testes/Enums/OperacaoTipoTddTest.php` → PASS=23 FAIL=0 (atualizado)
- `Testes/Sondas/WmsMovimentoSondaTddTest.php` → PASS=19 FAIL=0 (regressão)

## Correções pós-review (ciclo 1)

### B1 — BLOCKER: lista da sessão sem origem/destino

**Problema** (do review): A ação `listar` devolve `missao_logistica_item` (sem origem/destino, qty=1 fixo),
mas `movimentacao.js::_renderLista` esperava linhas brutas de `wms_movimentos` com `localizacao_codigo_origem`/`_destino`.

**O que foi feito:**

- `AlocacaoWms.php` — adicionado `use WmsMovimentoSonda` e nova ação `listarMovimentos` no `match`. O método `listarMovimentos()` chama `WmsMovimentoSonda::listarPorMissao()`, filtra apenas `operacao_tipo='movimentacao'`, e serializa os objetos como arrays simples com os campos `localizacao_codigo_origem`, `localizacao_codigo_destino`, `quantidade` (float real), `produto_id`, `operacao_tipo` e `confirmada_em`. Retorna `['ok'=>true, 'movimentos'=>[...]]`.

- `movimentacao.js` — `_carregarLista()` passou a enviar `sinalGxData: 'listarMovimentos'` (antes `'listar'`). O fallback `r.itens` foi removido (o novo endpoint sempre devolve `r.movimentos`). O comentário do bloco foi corrigido para descrever o contrato real (nit n1 resolvido junto).

**Arquivos alterados:**
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/AlocacaoWms.php`
- `componentes/Missoes/Logistica/sinais/App/html/operacaowms/movimentacao.js`

### M1 — MAJOR: extrato de auditoria sem origem/destino

**Problema** (do review): `ConfirmarAlocacaoLaboratorio.php` gravava `localizacao_codigo = $locCodigo` no extrato, mas na movimentação `$locCodigo` é `''` (o front envia `localizacao_codigo_origem`/`_destino`, não `localizacao_codigo`).

**O que foi feito:**

- `ConfirmarAlocacaoLaboratorio.php` — o payload do extrato usa agora `$locExtratoBase = $locCodigo ?: ($locDestinoCodigo ?: null)`. Para movimentação, `$locCodigo` é `''` e `$locDestinoCodigo` é o destino real, então `localizacao_codigo` no extrato passa a apontar para o destino efetivo. Adicionalmente, quando `$locOrigemCodigo` ou `$locDestinoCodigo` não são vazios, são incluídos como `localizacao_codigo_origem` e `localizacao_codigo_destino` no payload do extrato (campos extras ignorados pelas demais operações que não os enviam).

**Arquivo alterado:**
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/ConfirmarAlocacaoLaboratorio.php`

### Verificações do ciclo 1

- `php -l AlocacaoWms.php` → OK
- `php -l Laboratorios/ConfirmarAlocacaoLaboratorio.php` → OK
- `node --check movimentacao.js` → OK

---

## Correções pós-review (ciclo 2)

### M2 — MAJOR: `produto_nome` null na listagem de movimentos

**Problema** (review ciclo 2): `listarMovimentos` devolvia `$row->produto_nome ?? null`, mas a coluna
`produto_nome` não existe em `wms_movimentos` (só existe `produto_id`). O nome vive em
`missao_logistica_itens.dados_adicionais` (JSON). Toda linha do card exibia "Produto #ID".

**O que foi feito:**

- `AlocacaoWms.php` — adicionado `use MissaoLogisticaItem`. No método `listarMovimentos`, após
  montar o array de linhas (com `produto_nome => null` temporário), chama o novo método privado
  `resolverNomesProdutos($missaoId, $produtoIds)` em batch: faz `MissaoLogisticaItem::findAny(..., all=true)`
  uma única vez para todos os itens da missão, lê `dados_adicionais['produto_nome']` exatamente
  como `ListarAlocacoesLaboratorio` faz (linhas 61-68), monta `produto_id => nome` e aplica em
  cada movimento via loop. Sem N+1, sem raw SQL.

**Arquivo alterado:**
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/AlocacaoWms.php`

### m1 — Variável `$item` morta no cron

**Problema**: `foreach ($itensErp as $i => $item)` / `foreach ($itensWms as $i => $item)` nunca
usavam `$item` — reliam `$itensErp[$i]` / `$itensWms[$i]` diretamente.

**O que foi feito:** substituído por `foreach (array_keys($itensXxx) as $i)` com `$itensXxx[$i]`
explícito. Mesmo comportamento, sem variável morta.

**Arquivo alterado:**
- `crons/wms_alocacao_integracao.php`

### n2 — `"Total"` hardcoded sem `show()`

**Problema**: `<div class="app-stat-rotulo">Total</div>` sem internacionalização (a chave `md5("Total")`
já estava harvestada nos 3 idiomas).

**O que foi feito:** substituído por `<?= show('Total') ?>`.

**Arquivo alterado:**
- `componentes/Missoes/Logistica/sinais/App/html/operacaowms/movimentacao.php`

### Asserção de cobertura M2

Adicionado bloco "7. M2: listarMovimentos devolve produto_nome populado" em
`MovimentacaoE2ETddTest.php`: instancia `AlocacaoWms` via `ReflectionClass::newInstanceWithoutConstructor`,
expõe `listarMovimentos` com `setAccessible(true)`, invoca com a `missao_id` do cenário já existente
e assere que (a) `ok=true`, (b) há itens, (c) `produto_nome` está presente e não-nulo, (d) zero
movimentos sem nome na lista.

**Arquivo alterado:**
- `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/MovimentacaoE2ETddTest.php`

### Verificações do ciclo 2

- `php -l AlocacaoWms.php` → OK
- `php -l crons/wms_alocacao_integracao.php` → OK
- `php -l componentes/Missoes/Logistica/sinais/App/html/operacaowms/movimentacao.php` → OK
- `php -l Testes/Laboratorios/MovimentacaoE2ETddTest.php` → OK

---

## Próximos passos para o frontend

### Tela nova necessária

Criar `componentes/Missoes/Logistica/sinais/App/html/operacaowms/movimentacao.php` + `movimentacao.js` (passo 7 do techplan §12). O Handler existente já injeta `operacaoTipo`/`precisa*` e despacha pela `viewExecucao()` do adapter — zero mudança no Handler.

### Endpoint utilizado

Mesmo endpoint das demais operações: `POST /missao/Logistica/AlocacaoWms` com `sinalGxData` no payload.

### Ações disponíveis

| `sinalGxData` | Uso na movimentação |
|---|---|
| `confirmar` | Confirma o movimento (origem→produto→qty→destino) |
| `consultarProduto` | Resolve produto por código de barras (sem gravar) |
| `listar` | Lista itens da missão (alocação/inventário — NÃO usar na movimentação) |
| `listarMovimentos` | Lista movimentos da sessão (linhas brutas de `wms_movimentos`, filtradas por `operacao_tipo='movimentacao'`) — usar na movimentação |

### Payload de confirmação

```json
{
  "sinalGxData": "confirmar",
  "missao_id": 123,
  "produto_id": 456,
  "localizacao_codigo_origem": "CORREDOR-A-01",
  "localizacao_codigo_destino": "CORREDOR-B-05",
  "quantidade": 10,
  "origem_leitura": "codigo_barras"
}
```

`localizacao_codigo` (campo das demais operações) não precisa ser enviado; pode ser omitido ou vazio.

### Respostas de erro validarOperacao (AC4.7 — nada gravado)

| Condição | Mensagem retornada |
|---|---|
| `produto_id` ausente | `'ID do produto é obrigatório.'` |
| `localizacao_codigo_origem` vazia | `'Endereço de origem é obrigatório.'` |
| `localizacao_codigo_destino` vazia | `'Endereço de destino é obrigatório.'` |
| origem == destino | `'Origem e destino devem ser endereços diferentes.'` |
| `quantidade` null/`''`/<=0 | `'Quantidade é obrigatória e deve ser positiva.'` |
| produto não no catálogo | `'Produto não encontrado no catálogo.'` |

### Status codes HTTP

Sempre HTTP 200; sucesso/erro vem no JSON `{ok: true|false, erro?: string}` (padrão do sinal).

### Smoke de designação

O seletor `#select-operacao-wms` já é data-driven via `OperacaoAdapterFactory::operacoesSuportadas()`. Com `'movimentacao'` registrado no factory, "Movimentação" aparece automaticamente como opção — zero código novo de designação (confirmar AC1.1 no browser).

### Nota de listagem (§13 risco 4 — resolvido no ciclo 1)

`listarMovimentos` devolve `{ ok: true, movimentos: [...] }` com linhas brutas de `wms_movimentos`
filtradas por `operacao_tipo='movimentacao'`, contendo `localizacao_codigo_origem` e
`localizacao_codigo_destino` e a `quantidade` real de cada movimento. A tela já implementada em
`movimentacao.js` consome essa ação e renderiza o par origem→destino corretamente.

`listar` continua servindo alocação/inventário (devolve `itens` de `missao_logistica_item`) — não
usá-la para movimentação.
