# Aprovação por movimento nas Operações WMS — Spec

> Data: 2026-06-25 · Feature: operacao-wms (sub-feature) · Tipo: spec de design (brainstorming)
> Pré-requisitos: a Missão de Operação WMS genérica (`operacao_tipo`, `wms_movimentos`,
> adapters) já está implementada — ver `02-techplan.md`. Esta spec adiciona um **gate de
> aprovação do gestor, movimento a movimento**, sobre o que já existe.

## 1. Problema / intenção

Hoje a Missão de Operação WMS é **sessão livre que auto-conclui**: o operador registra
movimentos (`wms_movimentos`, `status='confirmada'`) e cada movimento fica **imediatamente
elegível** para o push assíncrono (cron → ERP/WMS). O checkbox "Requer Aprovação ao Concluir"
do painel de designação **não tem efeito** em WMS porque a criação força
`requer_aprovacao='0'` (`GestaoMissoesAlocacaoWms::processarEnvio`).

Precisamos que, **nas operações WMS**, o gestor revise o trabalho do operador **um movimento
de cada vez** antes que ele vá para a integração: um switch por movimento (confirma / não
confirma), persistido no banco a cada toggle, com um envio final que aplica só os confirmados.

Isso é especialmente crítico em inventário (cada movimento é uma contagem que ajusta estoque
no ERP/WMS) e em movimentação/alocação (vínculos produto×endereço que não devem subir errados).

## 2. Decisões tomadas (brainstorming)

| # | Decisão | Escolha |
|---|---------|---------|
| D1 | Unidade aprovada | **O movimento** (`wms_movimentos`), não o item designado (`missao_logistica_itens`). |
| D2 | Movimento não-confirmado no "Enviar" | **Cancelado/descartado** (`status='cancelada'`); a missão fecha (`CONCLUIDA`) no envio. |
| D3 | Estado inicial dos switches | **Todos já confirmados** (gestor só desmarca exceções). |
| D4 | Modelo de retenção | **Coluna nova `aprovacao_gestor`** (default `aprovado`); gate vira `status='confirmada' AND aprovacao_gestor='aprovado'`. |
| D5 | Default do checkbox em WMS | **Marcado** (exige aprovação por padrão; quem quiser auto-concluir desmarca). |

## 3. Modelo de dados

Migration nova `AlocacaoWms/Migrations/005_add_aprovacao_gestor.sql` (Postgres, idempotente):

```sql
ALTER TABLE wms_movimentos
  ADD COLUMN IF NOT EXISTS aprovacao_gestor VARCHAR(20) NOT NULL DEFAULT 'aprovado';
```

Semântica de `aprovacao_gestor`:

- **`aprovado`** — liberado para integração. **Default** (cobre linhas antigas e movimentos de
  missão **sem** aprovação → zero regressão automática). Também o estado final dos confirmados.
- **`pendente`** — retido, aguardando o gestor. Carimbado na gravação quando a missão exige
  aprovação. Na UI de revisão, o switch aparece **ligado** (D3).
- **`rejeitado`** — o gestor desligou o switch. Continua retido; vira `status='cancelada'` no "Enviar".

Índice (otimização opcional, não bloqueante): estreitar o predicado dos índices parciais de
integração para `... AND aprovacao_gestor='aprovado'`, evitando varrer movimentos retidos.
Pode ficar como passo separado se a recriação de índice atrapalhar a migration idempotente.

## 4. Máquina de estados — por que nada vaza antes do "Enviar"

Esta é a invariante central do design:

1. **Gravação do operador** (missão com aprovação): `ConfirmarAlocacaoLaboratorio` carimba
   `aprovacao_gestor='pendente'`. `status='confirmada'` (inalterado) → o operador **continua
   vendo o próprio saldo local** durante a sessão; o cron **não** empurra (gate exige `aprovado`).
2. **Toggle do gestor** (persiste a cada clique): alterna **`pendente` ⇄ `rejeitado`**.
   - Ligar **não** grava `aprovado` — grava `pendente`. Como o gate exige `aprovado`, **o cron
     nunca empurra durante a revisão**. É isto que garante o "salva no banco e envia depois".
   - Movimento não tocado permanece `pendente` (switch ON por D3).
3. **Enviar** (commit único, transacional por missão):
   - `pendente → aprovado` (libera os confirmados para o cron),
   - `rejeitado → status='cancelada'` (mantém `aprovacao_gestor='rejeitado'` para auditoria),
   - missão `AGUARDANDO_APROVACAO → CONCLUIDA` pela via canônica.

## 5. Gate de integração (3 pontos)

Adicionar `AND aprovacao_gestor = 'aprovado'` em:

1. `WmsMovimentoSonda::buscarPendentesIntegracaoErp`
2. `WmsMovimentoSonda::buscarPendentesIntegracaoWms`
3. Query de descoberta de tenants (blocks) no cron `crons/wms_alocacao_integracao.php`

As demais consultas da Sonda (`listarPorMissao`, `listarPorLocalizacao`,
`saldoPorLocalizacaoProduto`, `mapaLocalizacaoConfirmadaPorProduto`, estornos) **não mudam** —
filtram por `status`, não por `aprovacao_gestor`, então a UX do operador e o estorno seguem iguais.

## 6. Carimbar `pendente` na gravação

- Novo enum `AlocacaoWms/Enums/AprovacaoGestor` (`APROVADO`/`PENDENTE`/`REJEITADO`),
  espelhando `StatusAlocacao`.
- `WmsMovimentoSonda::registrarMovimento` ganha parâmetro
  `AprovacaoGestor $aprovacao = AprovacaoGestor::APROVADO` (default não-quebrante) e grava a coluna.
- `ConfirmarAlocacaoLaboratorio::executar` já resolve `$missao` (via validator). Lê
  `$missao->requer_aprovacao`: se verdadeiro, passa `AprovacaoGestor::PENDENTE`; senão deixa o default.
  Nenhuma outra mudança no fluxo do operador.

## 7. Criação — destravar o "Requer Aprovação" em WMS

- `GestaoMissoesAlocacaoWms::processarEnvio`: **remover** a linha `$data['requer_aprovacao'] = '0'`
  e deixar o valor do checkbox (`input-requer-aprovacao`, já existente no form
  `enviar_unificado.php` + sync no `designar_unificado.js`) fluir até a persistência.
- Default em WMS = **marcado** (D5). O checkbox do painel já nasce `checked`; basta não forçar `0`.
- O `MissoesStatus::finalizar` **já** roteia para `AGUARDANDO_APROVACAO` quando
  `requer_aprovacao` é verdadeiro (linha ~364) — nenhuma mudança lá.
- Os cards de operação WMS no painel já existem (WIP atual) — fora do escopo desta spec.

## 8. Tela do gestor — revisão por movimento

Na `Designacoes/html/designacoes_lista.php`, para missão `ALOCACAO_WMS` em
`AGUARDANDO_APROVACAO`, trocar o botão de aprovação genérico por **"Revisar movimentos"**, que
abre um modal:

```
┌─ Revisar movimentos — Missão #1234 (Inventário produtos) ──────────┐
│  [Confirmar todos]  [Descartar todos]                    3 itens    │
├────────────────────────────────────────────────────────────────────┤
│ ● Produto X (cod 123)   Endereço A-01-02   qtd 7   João  14h28  [ON]│
│ ● Produto Y (cod 456)   Endereço A-01-03   qtd 0   João  14h31  [ON]│
│ ● Produto Z (cod 789)   A-05 → B-12        qtd 3   Ana   14h40  [ON]│
├────────────────────────────────────────────────────────────────────┤
│                                       [Cancelar]   [Enviar (3) ▸]   │
└────────────────────────────────────────────────────────────────────┘
```

- Colunas adaptam por `operacao_tipo`: alocação/inventário = endereço único; movimentação =
  `origem → destino`. Mostra produto (nome/código), quantidade, operador e hora.
- Switch **default ON** (D3); cada toggle faz POST e persiste na hora. Atalhos
  "Confirmar todos" / "Descartar todos".
- "Enviar (N)" indica quantos serão efetivamente enviados.
- i18n via `show('texto PT')` (convenção Galaxia).

## 9. Backend do gestor — Lab novo

`AlocacaoWms/Laboratorios/RevisarMovimentosLaboratorio` (rotas
`/labs/apis/Logistica/AlocacaoWms/RevisarMovimentosLaboratorio/{ação}`):

- **`listar`** (GET `missao_id`): movimentos da missão com `status='confirmada'` e
  `aprovacao_gestor IN ('pendente','rejeitado')`, já no formato da tabela (enriquecido com
  nome do produto). Escopo por tenant via `customerControl`.
- **`alternar`** (POST `{movimento_id, ligado}`): alterna `pendente ⇄ rejeitado` para um
  movimento. Guard: só age sobre movimentos da missão em `AGUARDANDO_APROVACAO` e do tenant atual.
- **`enviar`** (POST `{missao_id}`): batch `pendente→aprovado` + `rejeitado→status='cancelada'`;
  em seguida conclui a missão pela via canônica (`MissoesAprovacao`/`MissoesStatus` →
  `CONCLUIDA`), disparando o hook de AJUSTE_DADOS já existente para `ALOCACAO_WMS`. O cron
  então passa a enxergar os aprovados e os empurra.

Layering: as escritas em `wms_movimentos` ficam no sinal AlocacaoWms (dono da tabela); a
transição da missão usa a maquinaria do Satélite (acoplamento frouxo via `class_exists`,
como já fazem os hooks existentes).

## 10. Não-regressão (invariantes)

- Missão WMS **sem** aprovação (checkbox desmarcado) = idêntico a hoje (default `aprovado`,
  empurra na hora).
- Linhas existentes em `wms_movimentos` = `aprovado` por default → integração intacta.
- `status` mantém o significado atual (`confirmada`/`cancelada`); as views locais do operador
  e o estorno não mudam.
- Alocação, movimentação e inventário compartilham o mesmo gate — a feature é transversal.
- Payload de push e gate por `status='confirmada'` preservados (apenas **acrescidos** de `aprovacao_gestor`).

## 11. Plano de testes (TDD, padrão V2, `dev_gx_goldie` block 22, limpando resíduo)

- **Sonda `WmsMovimentoSonda`**: `registrarMovimento` grava `pendente` quando pedido e
  `aprovado` por default; `buscarPendentesIntegracao{Erp,Wms}` excluem `pendente`/`rejeitado`
  e incluem `aprovado`.
- **Lab `RevisarMovimentosLaboratorio`**: `listar` traz só os retidos da missão; `alternar`
  faz `pendente⇄rejeitado`; `enviar` libera os pendentes, cancela os rejeitados, conclui a
  missão e deixa o cron enxergar os aprovados.
- **Fluxo ponta a ponta**: criar missão WMS com `requer_aprovacao=1`, registrar movimentos
  (ficam `pendente`, cron não empurra), revisar (1 desligado), enviar → 1 `cancelada`, resto
  `aprovado`, missão `CONCLUIDA`, cron empurra só os aprovados.
- **Regressão (verde obrigatório)**: `GestaoMissoesAlocacaoWmsTddTest`, fluxo saldo-0 da
  alocação, e as suítes dos adapters de operação.

## 12. Fora de escopo

- Devolver movimento ao operador para refazer (D2 = descartar).
- Aprovação parcial em múltiplas rodadas / vários "Enviar" (o envio fecha a missão de uma vez).
- Mudança nos cards de operação do painel (já em WIP separado).
- `validacao_ocupacao` / `conferencia_localizacao` (operações ainda não implementadas).
