# Inventário Cíclico

Motor de **planejamento contínuo** de inventário: campanhas que distribuem a contagem do
estoque ao longo do tempo e geram, a cada dia, a missão daquele dia — sem interromper a operação.

É um sinal **planejador**, totalmente desacoplado da **execução**: a contagem em si, o registro
de movimentos e a sincronização ERP/WMS são do sinal `AlocacaoWms` (operações WMS). Este módulo
só **planeja, distribui, seleciona e mede**.

> 📋 **Todas as configurações possíveis** (campos do plano, universos, distribuição, seleção,
> calendário, política de divergência, exemplos prontos) estão em **[`CONFIGURACOES.md`](CONFIGURACOES.md)**.

## Onde acessar / configurar

| | Onde | Como |
|---|---|---|
| **Configurar campanhas** (admin) | tela do sinal: **`/missao/logistica/inventariociclico`** | botão **"+ Novo plano"** (todos os campos do `CONFIGURACOES.md`), pausar/ativar, **"Gerar ciclo de hoje"**, indicadores, **Divergências** |
| **Entrada no menu** | card **"Inventário Cíclico"** no menu de inventário (`App/html/inventario/menu.php`) | aponta para o sinal acima |
| **Executar a contagem** (operador) | tela já existente do **`AlocacaoWms`**: `/missao/logistica/app/alocacaowms` | o ciclo gera uma missão `ALOCACAO_WMS`; o operador conta lá; o cron sincroniza ERP/WMS |

> ⚠️ O sinal é nomeado **`Inventariociclico`** (uma só maiúscula) de propósito: é o que torna a
> rota web `/missao/logistica/inventariociclico` resolvível (o framework faz `ucwords` do segmento;
> nomes com maiúscula interna — ex. `AlocacaoWms` — só funcionam como sub-rota do `App`).

## Conceitos

| Conceito | Onde vive |
|---|---|
| **Plano** (campanha) | `inventario_planos` + `Laboratorios/CriarPlanoLaboratorio` |
| **Universo** (quais alvos) | `Universos/` (Registry+Interface) → materializado em `inventario_plano_alvos` |
| **Alvo** (genérico: endereço OU produto) | `Enums/AlvoTipo` |
| **Distribuição** (carga diária) | `Distribuicao/` (quantidade fixa, percentual, período) |
| **Seleção** (quais N pegar) | `Selecao/` (Registry+Interface) |
| **Calendário** | `Calendario/Calendario::deveRodarHoje` |
| **Ciclo** (a missão de um dia) | `inventario_ciclos` + `Laboratorios/GerarCicloDiarioLaboratorio` |
| **Ponte com a execução WMS** | `Bridge/MissaoInventarioBridge` |
| **Frequência/indicadores** | colunas de `inventario_plano_alvos` + `Laboratorios/IndicadoresPlanoLaboratorio` |

## Fluxo

```
Criar plano → define universo → materializa alvos → (cron diário às 04:00)
   → fecha ciclos abertos de ontem (atualiza frequência)
   → calendário manda rodar hoje?
   → sincroniza o universo (upsert: entra endereço/produto novo, sai o que sumiu,
       PRESERVA o histórico dos que ficam — RecalcularUniverso, não-destrutivo)
   → calcula carga do dia → seleciona próximos alvos
   → cria 1 missão AlocacaoWms (operacao_tipo=inventario_endereco/produtos)
   → operador conta na tela JÁ EXISTENTE do AlocacaoWms; ERP/WMS sincroniza pelo cron de lá
   → ao fechar o ciclo, lê wms_movimentos e avança o histórico
   → universo 100% coberto ⇒ nova campanha (rollover) — contínuo
```

## Genérico e plugável (estende sem tocar no core)

- **Universos**: `UniversoProviderRegistry::registrar('familia', FamiliaProvider::class)`
  (hoje: `enderecos`, `produtos`).
- **Seleção**: `SelecaoEstrategiaRegistry::registrar('abc', AbcSelecao::class)`
  (hoje: `sequencial`, `aleatoria`, `menos_recente`, `prioridade_manual`).
- **Distribuição**: `DistribuicaoFactory` (`quantidade_fixa`, `percentual`, `periodo`).

## Isolamento

A única dependência da execução é o `Bridge`, por duas costuras **genéricas e já existentes**:
- criar missão: `MissoesPersistencia::criacao()` (tipo `ALOCACAO_WMS`, alvos do dia em
  `configuracoes.inventario_ciclico`, rastreio por `referencia_tipo='INVENTARIO_CICLICO'`);
- ler de volta: `WmsMovimentoSonda::listarPorMissao()` por `missao_logistica_id`.

O `AlocacaoWms` **não é modificado**. O "roteiro do dia" (checklist de alvos) é exibido na tela do
próprio Inventário Cíclico, mantendo o `AlocacaoWms` 100% intocado.

## Fase 2 — Divergência, recontagem, escalonamento e ajuste (IMPLEMENTADA)

Spec em `PRD/inventario-ciclico-divergencia/01-spec.md`. Fluxo: ao fechar o ciclo,
`AvaliarDivergenciaLaboratorio` compara **contado × esperado** e registra divergência
acima da `tolerancia`; a `PadraoRecontagemPolitica` decide recontar (`GerarRecontagemLaboratorio`,
ciclo `tipo=recontagem`) ou escalar (`EscalonarDivergenciaLaboratorio`, reusa o gate
`aprovacao_gestor` do AlocacaoWms → tela `RevisarMovimentos`). O conferente aprova e o
`AjusteInternoAplicador` posta a correção no saldo.

**Seams plugáveis (interno hoje, WMS externo no futuro — troca de classe, sem mexer no motor):**
- **Esperado**: `Esperado/EsperadoLocalProvider` (saldo interno = soma de alocações) → `EsperadoWmsProvider`.
- **Ajuste**: `Ajuste/AjusteInternoAplicador` (posta movimento de correção delta) → `AjusteWmsAplicador`.

> Nota técnica: a correção usa `missao_logistica_id` do ciclo (FK NOT-NULL de `wms_movimentos`);
> por isso a divergência grava essa missão.

Config por plano (migration 004): `tolerancia`, `max_recontagens`, `auto_ajuste` (default 0 =
sempre exige conferente), `recontagem_habilitada`.

**Orquestração (no cron diário):** ao fechar um ciclo, `ProcessarDivergenciasCicloLaboratorio`
roda avaliar→política→recontar/escalar; `AplicarAjustesAprovadosLaboratorio` varre as divergências
escaladas e efetiva o ajuste do que o conferente aprovou. Ambos são chamados de forma **defensiva**
(try/catch) — se a fase 2 não estiver deployada num tenant, o cron segue normal.

**⚠️ Requisito do escalonamento:** o gate do AlocacaoWms (`alternarAprovacao`) só retém movimentos
`pendente`/`rejeitado` — não puxa um `aprovado` de volta. Logo, **planos que querem o fluxo de
conferente devem ter `requer_aprovacao=true`** (movimentos de contagem nascem retidos `pendente`; o
conferente decide em `RevisarMovimentos`; a varredura aplica o ajuste no que ele aprovar). Sem isso,
a divergência é detectada/escalada e gera recontagem, mas o ajuste só ocorre após aprovação no gate.
*(Auto-aprovar os itens NÃO divergentes é uma otimização de fase posterior.)*

## Fora de escopo (fases seguintes)

- **Plug do WMS externo** (`EsperadoWmsProvider` + `AjusteWmsAplicador`) — os seams já existem.
- **Estratégias/universos dependentes de dado ainda não modelado** no Genesis: curva ABC, giro,
  família, fornecedor, maior divergência histórica. Entram como adapters registráveis.
- **Auto-ajuste por tolerância** (coluna `auto_ajuste` já existe; default 0).
- **Dashboard rico** (tempo médio, produtividade por operador). Acuracidade já é exposta.

## Deploy — OBRIGATÓRIO (o módulo não funciona sem os dois passos)

O código sozinho fica **inerte**. Para o Inventário Cíclico operar é preciso:

### 1. Aplicar as migrations (pré-requisito de tudo)

Aplicar `Migrations/001..006` em **cada banco de tenant** (`gx_goldie`) — 001-003 (fase 1) +
004-006 (fase 2: política no plano, `inventario_divergencias`, campos de recontagem no ciclo):

```bash
for f in Migrations/0*.sql; do psql <db> -f "$f"; done
```

As tabelas carregam o conjunto **completo** de colunas de auditoria/block (espelhando
`wms_movimentos`) — exigido por `ModelInteligencia::create()` (ver memória
`modelinteligencia_create_audit_columns`). Sem as tabelas:
- a tela `/missao/logistica/inventariociclico` quebra ao listar/criar planos;
- o cron sai limpo via guard de deploy (não gera nada).

### 2. Agendar o cron diário (o que torna o inventário "cíclico")

```bash
# nome 0-4-... ⇒ auto.sh registra "0 4 * * *" (todo dia às 04:00)
bash crons/auto.sh
```

O cron `crons/0-4-inventario_ciclico_diario.php` é **quem gera as missões automaticamente**
todo dia (fecha o ciclo anterior + cria o do dia, por tenant). **Sem o cron agendado o módulo
não roda sozinho** — só dá pra gerar ciclo manualmente pelo botão "Gerar ciclo de hoje" na tela.

> Resumo: **migration = pré-requisito** (tabelas + tela + execução manual);
> **cron = automação** (geração diária dos ciclos). Os dois são necessários para o fluxo completo.

## Testes

- **Unidade / controller** (`Testes/**/**TddTest.php`, ~306 asserts): motor + divergência (fase 1+2),
  além de:
  - `Testes/Enums/EnumsTddTest.php` — labels/cssClass, predicados (`estadoFinal`,
    `elegivelParaScheduler`), máquina de transições do `PlanoStatus`, `operacaoTipoInventario`,
    `fromValor` (default não-quebrante).
  - `Testes/Ajuste/AjusteTddTest.php` — além do delta: guards (missão/produto/localização ≤ 0 → false;
    delta 0 → true idempotente, sem movimento).
  - `Testes/Bridge/MissaoInventarioBridgeTddTest.php` — `montarPayload` (operacao_tipo, roteiro,
    referencia_*, dia_efetivo, operadores) + `coberturaDaMissao` (keyed tipo:ref, mais recente vence).
  - `Testes/Controller/InventariociclicoControllerTddTest.php` — roteamento: `__call(Html)` renderiza
    (regressão do bug `::Html()`), `start([])`→telaPlanos, guarda de deploy (`erro_deploy`).
  - `Testes/Cron/CronSmokeTddTest.php` — smoke do cron diário (subprocesso): roda sem fatal,
    loga início, termina limpo, sai 0.
- **Integração** (dev_gx_goldie; isola `block`/seed, limpa no fim; exige as tabelas):
  - `Testes/Integracao/FluxoCicloIntegrationTest.php` — fase 1 (1 feliz + 5 variações).
  - `Testes/Integracao/DivergenciaIntegrationTest.php` — fase 2 (esperado/avaliar/ajuste real).
  - `Testes/Integracao/MaestroAjusteIntegrationTest.php` — maestro + ajuste efetivado.
  - `Testes/Integracao/RecalcularUpsertIntegrationTest.php` — sincronização do universo
    PRESERVA o histórico dos alvos que ficam (semeia, materializa, marca inventariado, muda a
    fonte, re-sincroniza, confere que `qtd_inventarios`/`coberto_campanha` sobrevivem).
  - `Testes/Integracao/MigrationsIdempotenteIntegrationTest.php` — aplica 001–006 duas vezes
    (idempotência `IF NOT EXISTS`) + confere tabelas/colunas.
- **Designação avulsa de endereço (Opção b)** vive no sinal `Designacoes`:
  - `Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php` — `buscarItens=[]` p/
    inventário de endereço + `inventario_alvos` no config.
  - `Designacoes/Testes/Strategies/InventarioEnderecoFlowTddTest.php` — integração com seed real de
    `wms_localizacoes`: seletor de endereços, rótulo, resolução IDs→códigos, escopo gravado.
