# Inventário Cíclico — Fase 2: Divergência, Recontagem, Escalonamento e Ajuste

> Continuação do motor de planejamento (fase 1). Esta fase trata o que acontece
> quando a contagem **não bate** com o sistema: detectar, recontar, escalar ao
> conferente e ajustar o estoque. Spec acionável — pode ser refinada pelo
> `genesis-techlead` em `02-techplan.md`.

## Context

A fase 1 entrega planos que geram, todo dia, uma missão de inventário (reusando a
execução do `AlocacaoWms`) e medem cobertura. O operador conta cada produto×endereço
(cada contagem é um movimento em `wms_movimentos` com `quantidade`). **Falta o ciclo de
qualidade**: comparar contado × esperado, recontar quando diverge, escalar ao conferente
quando persiste e refletir o ajuste no estoque.

**Premissa de integração (importante):** hoje a operação WMS é **100% interna ao Genesis**
— nada entra de fora. Logo o **saldo do Genesis É a fonte da verdade** e o ajuste é uma
**escrita interna**. **No futuro um WMS externo será plugado**; por isso o esperado e o ajuste
são encapsulados em **seams plugáveis** (provider/aplicador) que trocam de implementação sem
mexer no motor. Não há dependência externa nem risco de integração nesta fase.

## Decisões travadas (com o PO)

| Decisão | Escolha | Implicação |
|---|---|---|
| Fonte do **esperado** | **Saldo interno do Genesis** | Operação é interna → o saldo do Genesis é autoritativo (sem ressalva de ruído). Encapsulado em `EsperadoProvider`: **`EsperadoLocalProvider` hoje** → `EsperadoWmsProvider` quando o WMS for plugado. |
| **Ajuste de estoque** | **Correção interna do saldo** | Posta um **movimento de correção** em `wms_movimentos` (append-only) levando o saldo ao valor contado. Encapsulado em `AjusteAplicador`: **`AjusteInternoAplicador` hoje** → `AjusteWmsAplicador` (push) quando o WMS for plugado. |
| **Escalonamento** | **Reusar gate do AlocacaoWms** | `aprovacao_gestor` + tela `RevisarMovimentos` + `commitAprovacao`. Conferente = papel gestor. |
| **Auto-ajuste** | **Sempre exige conferente** | `auto_ajuste=false` por padrão (config por plano). Nada mexe no estoque sem humano. |

## Definição de "esperado" (saldo local) e divergência

- **esperado(produto, endereço)** = soma das `quantidade` de movimentos **confirmados de
  alocação** (`operacao_tipo='alocacao'`) daquele produto×endereço, **anteriores ao ciclo**
  (exclui inventários e o próprio ciclo — não pode ser circular).
- **contado(produto, endereço)** = `quantidade` do movimento de inventário do ciclo.
- **divergência** = `contado − esperado`.
- Casos:
  - **sobra**: contado > esperado (inclui "produto não esperado": esperado 0, contado > 0).
  - **falta**: contado < esperado (inclui "produto faltante": esperado > 0, não contado → conta como 0, **se** `falta_conta_como_zero=true` na política).
- **`EsperadoLocalProvider`** calcula o esperado de alocações **anteriores ao ciclo** (filtra por
  `operacao_tipo='alocacao'` e exclui os movimentos do próprio ciclo — não pode ser circular).
  Como a operação é interna, esse saldo é autoritativo. Quando o WMS externo for plugado, troca-se
  por `EsperadoWmsProvider` (lê o saldo do WMS) — o motor não muda.

### Mecânica do ajuste interno (saldo = soma de movimentos)

Hoje o saldo é a **soma das `quantidade` confirmadas** do endereço×produto. Por isso o ajuste
**não pode** ser "somar a contagem" (distorceria o saldo). O `AjusteInternoAplicador` posta um
**movimento de correção** com delta = `contado − esperado`, de modo que o novo saldo passe a ser
exatamente o contado — mantendo o ledger append-only e auditável (operador/conferente/timestamp).
Esse mesmo delta (ou o valor absoluto) é o que o futuro `AjusteWmsAplicador` empurraria pro WMS.

## Fluxo

```
Operador conta (ciclo da fase 1)
   │
   ▼
Fecha o ciclo → AvaliarDivergenciaLaboratorio
   │   para cada alvo contado: esperado (provider) × contado
   │   |dif| <= tolerância?  ── sim ──▶ OK (sem divergência)
   │            │ não
   ▼            ▼
Registra divergência (inventario_divergencias, rodada=N)
   │
   ├─ rodada < max_recontagens ──▶ GerarRecontagem (novo ciclo só dos alvos divergentes)
   │                                    └▶ operador reconta → volta ao topo (rodada+1)
   │
   └─ recontagem CONFIRMA a contagem (bate com a rodada anterior) e ainda diverge do sistema,
      OU rodada == max_recontagens
                 │
                 ▼
        Escalona: movimentos do alvo → aprovacao_gestor='pendente'
                 │   (conferente revisa em RevisarMovimentos)
                 ▼
        Conferente decide:
           aprova  → AjusteAplicador efetiva (hoje: correção interna do saldo) (auto_ajuste=false: sempre aqui)
           rejeita → contagem descartada, sem ajuste
```

Princípio: **a recontagem valida a CONTAGEM** (duas contagens batendo = contagem confiável);
se a contagem confiável ainda diverge do sistema, o problema é o estoque → escala p/ ajuste.

## Modelo de dados (migrations aditivas; colunas de auditoria completas — ver `modelinteligencia_create_audit_columns`)

**`inventario_planos`** (+ colunas de política):
- `tolerancia` NUMERIC(14,4) DEFAULT 0 — limiar de divergência ignorável
- `max_recontagens` INT DEFAULT 1
- `auto_ajuste` SMALLINT DEFAULT 0 — sempre 0 nesta fase (conferente obrigatório)
- `recontagem_habilitada` SMALLINT DEFAULT 1

**`inventario_divergencias`** (nova):
- `id`, `plano_id`, `ciclo_id`, `alvo_id`, `produto_id`, `localizacao_codigo`
- `qtd_esperada`, `qtd_contada`, `diferenca` NUMERIC(14,4)
- `rodada` INT (1ª contagem, 2ª, …)
- `status` VARCHAR(20): `aberta | recontagem | escalada | ajustada | resolvida | descartada`
- `conferente_id` INT, `resolvida_em` TIMESTAMP
- + bloco de auditoria/block padrão

**`inventario_ciclos`** (+ encadeamento de recontagem):
- `tipo` VARCHAR(20) DEFAULT 'normal' (`normal | recontagem`)
- `rodada` INT DEFAULT 0, `ciclo_origem_id` INT (ciclo que originou a recontagem)

**`inventario_plano_alvos.ultima_divergencia`** (já reservada na fase 1) → passa a ser atualizada.

## Componentes (V2, padrão do módulo)

- **`Esperado/` (Registry+Interface)** — `EsperadoProviderInterface::esperado(produto, endereço, antesDe): float`;
  `EsperadoLocalProvider` (soma de alocações via `WmsMovimentoSonda`). **Seam plugável** → `EsperadoWmsProvider` no futuro.
- **`Ajuste/` (Registry+Interface)** — `AjusteAplicadorInterface::aplicar(divergencia): void`;
  `AjusteInternoAplicador` (posta movimento de correção em `wms_movimentos`). **Seam plugável** →
  `AjusteWmsAplicador` (push pro WMS) no futuro. Simétrico ao Esperado.
- **`Laboratorios/AvaliarDivergenciaLaboratorio`** — ao fechar o ciclo, compara e registra
  divergências (consome `EsperadoProvider`; injetável p/ TDD).
- **`Politica/` (Registry+Interface)** — política de recontagem/escalonamento. Default
  `PadraoRecontagemPolitica`: reconta até `max_recontagens`; escala quando 2 contagens batem
  entre si mas divergem do sistema, ou ao atingir o máximo.
- **`Laboratorios/GerarRecontagemLaboratorio`** — cria ciclo `tipo=recontagem` só com os alvos
  divergentes (reusa o `MissaoInventarioBridge`, com `tipo/rodada` nas `configuracoes`).
- **`Laboratorios/EscalonarDivergenciaLaboratorio`** — marca os movimentos do alvo como
  `aprovacao_gestor='pendente'` (reusa `WmsMovimentoSonda::alternarAprovacao`) e move a
  divergência p/ `escalada`.
- **`Enums/`** — `DivergenciaStatus`, `CicloTipo`.
- **Indicadores** (estende `IndicadoresPlanoLaboratorio`) — `divergencias`, `acuracidade`
  (`1 − divergentes/contados`).
- **Tela** — aba "Divergências" no detalhe do plano (lista + status); a decisão do conferente
  reusa a tela `RevisarMovimentos` do AlocacaoWms (sem duplicar).

## Reuso (o que NÃO precisa ser construído)

- **Gate de aprovação**: `aprovacao_gestor`, `alternarAprovacao`, `alternarAprovacaoTodos`,
  `commitAprovacao`, `listarParaRevisao`, tela `RevisarMovimentos`.
- **Ledger de saldo**: `WmsMovimentoSonda` (append-only) — o `AjusteInternoAplicador` posta a
  correção aqui; `saldoPorLocalizacaoProduto` recomputa o saldo certo.
- **Motor de ciclo/missão**: recontagem é só um ciclo de subconjunto (reusa fase 1 + bridge).
- *(Futuro)* o `payloadPushErp`/cron de push já existente vira a base do `AjusteWmsAplicador`
  quando o WMS externo for plugado — sem reescrever o motor.

## Critérios de aceite

- **AC1** — Ao fechar um ciclo, alvo com `|contado − esperado| > tolerância` gera registro em
  `inventario_divergencias` (rodada 1).
- **AC2** — Divergência **dentro** da tolerância não gera divergência, recontagem nem escala.
- **AC3** — Com `recontagem_habilitada` e `rodada < max_recontagens`, a divergência gera um
  ciclo `tipo=recontagem` contendo **apenas** os alvos divergentes.
- **AC4** — Recontagem que **confirma** a contagem anterior (mesma quantidade) e ainda diverge do
  sistema → **escala** (`aprovacao_gestor='pendente'`), **não ajusta sozinha**.
- **AC5** — Conferente **aprova** em `RevisarMovimentos` → `AjusteInternoAplicador` posta o
  movimento de correção e o saldo do endereço×produto passa a ser o **contado**; divergência →
  `ajustada`. **Rejeita** → contagem `cancelada`, saldo intacto; divergência → `descartada`.
- **AC6** — `auto_ajuste=false` (default): nenhum ajuste de estoque ocorre sem decisão do conferente.
- **AC7** — Indicadores do plano passam a expor `divergencias` e `acuracidade`.
- **AC8** — Tudo escopado por tenant (customerControl) e idempotente (recontagem não duplica
  ciclo no mesmo dia; avaliação não duplica divergência do mesmo ciclo/alvo).

## Fora de escopo (fase 2)

- **Plug do WMS externo** (`EsperadoWmsProvider` + `AjusteWmsAplicador`) — os **seams ficam prontos**
  nesta fase; a implementação real entra quando o WMS for plugado, sem tocar no motor.
- **Auto-ajuste por tolerância** — a coluna `auto_ajuste` já existe; ligar/automatizar é fase posterior.
- Curva ABC / giro / família / fornecedor (segue dependendo de dado não modelado).
- Conferente como papel/fila dedicada (decidido: reusar gate do gestor).

## Dependências e riscos

- **🟢 Sem dependência externa nesta fase** — operação interna; esperado e ajuste são internos.
  O "risco de integração" só aparece quando o WMS externo for plugado — e o design já o isola nos
  dois seams (`EsperadoProvider` + `AjusteAplicador`): é troca de classe, não retrabalho.
- **🟡 Correção do saldo via movimento (append-only, não mutação)** — `AvaliarDivergencia` deve
  calcular o esperado **antes** do ciclo (excluindo a contagem e a própria correção) p/ não entrar
  em laço. Coberto por idempotência (AC8).
- **🟡 Conferente = gestor** — confirmar que o papel/permissão de quem revisa em `RevisarMovimentos`
  atende o conceito de "conferente"; se precisar separar, vira o papel `CONFERENTE` (já há `papel`
  em `missao_logistica_usuario`).
- **🔵 (Futuro) Plug do WMS externo** — quando existir, definir com o WMS: esperado vem de um read?
  ajuste é set absoluto ou delta? idempotência do push? **Hoje não bloqueia nada** — fica como o
  trabalho do `EsperadoWmsProvider`/`AjusteWmsAplicador`.

## Ordem de implementação

1. Migrations (política no plano + `inventario_divergencias` + campos de ciclo).
2. `EsperadoProvider` (Registry) + `EsperadoLocalProvider` — **TDD**.
3. `AvaliarDivergenciaLaboratorio` + `PadraoRecontagemPolitica` — **TDD** (fakes).
4. `GerarRecontagemLaboratorio` (reusa bridge) — **TDD**.
5. `EscalonarDivergenciaLaboratorio` (wire no `aprovacao_gestor`).
6. Indicadores (+ acuracidade) + aba de divergências; conferente via `RevisarMovimentos`.
7. Integração (dev_gx_goldie) + testes de fluxo (espelhar o `FluxoCicloIntegrationTest`).
