# Missão de Operação WMS — guia do módulo

> Sinal `AlocacaoWms` (o diretório mantém o nome legado por compatibilidade), mas
> conceitualmente é a **Missão de Operação WMS**: um componente de missão logística
> **genérico**, dirigido por `operacao_tipo`, que atende várias operações de
> estoque/localização — não só alocação.
>
> Operações previstas: **alocação** (produto→endereço), **inventário de endereço**,
> **inventário de produtos**, **conferência por localização**, **validação de
> ocupação**. Hoje `alocacao`, `inventario_produtos` e `inventario_endereco` estão
> usáveis ponta a ponta; faltam `conferencia_localizacao` e `validacao_ocupacao`.
> Ver **Status** no fim.
>
> Specs/decisões: `PRD/operacao-wms/` (00-prd, 01-spec, 02-techplan). Origem
> histórica (só alocação): `PRD/alocacao-wms/`.

---

## 1. Ideia central (leia isto primeiro)

O **framework de missões** (`Constelacoes/Logistica/MissaoLogistica`: tabela
`missao_logistica`/`missao_logistica_itens`, `TipoMissao`, `StatusMissao`,
`GestaoMissoesBase`, `BaseHandler`, factories) **já é genérico**. Este módulo NÃO
cria um tipo de missão por operação. Em vez disso:

- **Um único tipo de missão** — `TipoMissao::ALOCACAO_WMS` (id 14). O nome/valor é
  mantido por compatibilidade; **não renomear** (há linhas em produção com
  `tipo_missao_id=14` e `referencia_tipo='ALOCACAO_WMS'`).
- **A operação concreta vive em `configuracoes.operacao_tipo`** (enum `OperacaoTipo`).
  Default não-quebrante: `alocacao` (missões antigas sem o campo).
- **Cada operação é isolada num `OperacaoAdapter`** (registry). O adapter concentra
  TUDO que é específico da operação: validação, montagem do movimento, view de
  execução, payload de push. O resto (Validator transversal, Laboratório, Handler,
  hooks, cron) é genérico e **não muda** ao adicionar operação.

> **Regra de ouro:** adicionar uma operação = **1 `*Adapter` + 1 linha no factory +
> 1 tela**. Se você está mexendo no Validator/Laboratório/Handler/hooks/cron para
> adicionar uma operação, provavelmente está no lugar errado — vá para o adapter.

---

## 2. Mapa de diretórios (índice)

```
AlocacaoWms/
├── AlocacaoWms.php                 # Dispatcher (controller fino). Roteia ações do front:
│                                   #   confirmar | consultarEndereco | consultarProduto
│                                   #   | removerProduto | listar | historico
│
├── Enums/
│   ├── OperacaoTipo.php            # ★ Discriminador da operação (alocacao, inventario_*, ...)
│   │                              #   fromConfig() (default ALOCACAO), exigeQuantidade/Endereco/Produto
│   ├── StatusAlocacao.php          # Status do MOVIMENTO: confirmada | cancelada
│   ├── EstadoIntegracao.php        # pendente | enviado | falha (por movimento, ERP e WMS independentes)
│   └── OrigemLeitura.php           # codigo_barras | manual
│
├── Strategies/Operacao/            # ★ CORAÇÃO da generalização (o registry de operações)
│   ├── OperacaoAdapterInterface.php  # Contrato de uma operação
│   ├── OperacaoAdapterBase.php        # Defaults (precisa*, montarMovimento, payloadPush*)
│   ├── OperacaoAdapterFactory.php     # Registry operacao_tipo => *Adapter (create / createFromConfig)
│   ├── AlocacaoAdapter.php            # operacao_tipo = alocacao (referência, em produção)
│   ├── InventarioAdapterBase.php      # base comum dos inventários (contagem: qty obrigatória + push de contagem)
│   ├── InventarioProdutosAdapter.php  # inventario_produtos (produto-first; usável)
│   └── InventarioEnderecoAdapter.php  # inventario_endereco (endereço-first; usável)
│
├── Validators/
│   └── ConfirmarAlocacaoValidator.php # Gate TRANSVERSAL (missão ativa + permissão);
│                                      #   delega o específico a adapter->validarOperacao()
│
├── Laboratorios/                   # Casos de uso (orquestram Validator → Adapter → Sonda)
│   ├── ConfirmarAlocacaoLaboratorio.php  # Confirma 1 movimento (item dinâmico + status + extrato)
│   ├── ConsultarEnderecoLaboratorio.php  # Contexto do endereço ativo + saldo local
│   ├── ListarAlocacoesLaboratorio.php    # Itens da missão enriquecidos + totais por produto/local
│   └── RemoverAlocacaoLaboratorio.php    # "Voltar atrás": soft-delete (status=cancelada)
│
├── Sondas/                         # Models V2 (ModelInteligencia)
│   ├── WmsMovimentoSonda.php        # ★ Tabela wms_movimentos (log append-only de movimentos)
│   └── WmsLocalizacaoSonda.php      # Tabela wms_localizacoes (catálogo de endereços; só leitura)
│
├── Migrations/                     # Postgres (gx_goldie / dev_gx_goldie)
│   ├── 001_create_wms_localizacoes.sql
│   ├── 002_create_wms_alocacoes.sql            # cria a tabela base (nome legado)
│   ├── 003_seed_tipo_missao_alocacao_wms.sql   # seed tipos_missao_logistica id=14
│   └── 004_generalizar_para_wms_movimentos.sql # ★ rename -> wms_movimentos + operacao_tipo + ocupacao_estado
│
├── Testes/                         # TddTest CLI (espelham a árvore). Rodam em dev_gx_goldie.
│   ├── Enums/  Strategies/Operacao/  Validators/  Laboratorios/  Sondas/  Handlers/
│
└── README.md                       # este arquivo
```

### Pontos relacionados FORA do sinal (também fazem parte do fluxo)

| Camada | Arquivo | Papel |
|---|---|---|
| Tipo de missão | `Constelacoes/.../Enums/TipoMissao.php` | `ALOCACAO_WMS` (id 14) — o tipo genérico |
| Criação (gestor) | `Designacoes/Laboratorios/GestaoMissoesAlocacaoWms.php` | `getConfiguracoesAdicionais` grava `operacao_tipo` (gated pelo registry) |
| Criação (form/UI) | `Designacoes/Strategies/Designacao/AlocacaoWmsStrategy.php` | seleção de produtos, `getOperacoesSuportadas()`, form config |
| Execução (operador) | `App/Handlers/AlocacaoWmsHandler.php` | resolve adapter → `viewExecucao()` → renderiza por operação |
| Execução (views) | `App/html/alocacaowms/*` | tela legada da alocação (`item.php`/`item.js`) |
| Integração push | `Constelacoes/.../Strategies/Erp\|WmsAlocacaoWmsHookStrategy.php` | usam `adapter->operacaoPush*()` + `payloadPush*()` |
| Integração (worker) | `crons/wms_alocacao_integracao.php` | varre `wms_movimentos` pendentes e dispara os hooks |

---

## 3. O contrato de operação (`OperacaoAdapter`)

Tudo que é específico de uma operação está em **um** lugar. Métodos do
`OperacaoAdapterInterface` (defaults no `OperacaoAdapterBase`):

| Método | Para que serve | Default no Base? |
|---|---|---|
| `getOperacaoTipo(): OperacaoTipo` | identidade | — (abstrato) |
| `validarOperacao(array $dados, object $missao): array` | validação específica (produto/localização/quantidade/…) após o gate transversal. Retorna `{ok, erro?, produto?, localizacao?, quantidade?}` | — (abstrato) |
| `viewExecucao(): string` | caminho da tela do operador (relativo ao sinal App) | — (abstrato) |
| `montarMovimento(array $contexto): array` | campos para `WmsMovimentoSonda::registrarMovimento` (carimba `operacao_tipo`) | ✅ pass-through |
| `precisaEndereco/Produto/Quantidade(): bool` | dicas de fluxo p/ a view | ✅ delega ao `OperacaoTipo` |
| `operacaoPushErp/Wms(): string` | nome do `opc` enviado ao ERP/WMS | — (abstrato) |
| `payloadPushErp/Wms(array $item, int $missaoId): array` | payload de push (sempre com `operacao_tipo`) | ✅ shape legado + operacao_tipo |

O `OperacaoAdapterFactory` é o registry (espelha `DesignacaoStrategyFactory`):

```php
private static array $adapters = [
    'alocacao'            => AlocacaoAdapter::class,
    'inventario_produtos' => InventarioProdutosAdapter::class,
    'inventario_endereco' => InventarioEnderecoAdapter::class,  // só registrar quando a tela existir
];
OperacaoAdapterFactory::create(OperacaoTipo::ALOCACAO);          // lança se não registrado
OperacaoAdapterFactory::createFromConfig($configuracoes);        // default não-quebrante = alocacao
OperacaoAdapterFactory::operacoesSuportadas();                   // ['alocacao'] -> alimenta o seletor
```

---

## 4. Ciclo de vida de uma operação (fim a fim)

```
CRIAÇÃO (gestor, sinal Designacoes)
  AlocacaoWmsStrategy (form/seleção) → GestaoMissoesAlocacaoWms::getConfiguracoesAdicionais
  grava configuracoes.operacao_tipo (validado contra operacoesSuportadas; default alocacao)
        │
        ▼
EXECUÇÃO (operador, sinal App)
  AlocacaoWmsHandler::AlocacaoWms_item
    → OperacaoTipo::fromConfig(configuracoes) → OperacaoAdapterFactory::create → adapter
    → renderiza adapter->viewExecucao()  (alocacao = html/alocacaowms/item)
        │  (a view bipa e chama o dispatcher AlocacaoWms.php via AJAX)
        ▼
CONFIRMAÇÃO (dispatcher → Laboratório)
  AlocacaoWms.php (ação 'confirmar') → ConfirmarAlocacaoLaboratorio::executar
    1. ConfirmarAlocacaoValidator::validar  → transversal (missão ativa + permissão)
                                            → adapter->validarOperacao (específico)
    2. findOrCreateItem  (item dinâmico — sessão livre)
    3. adapter->montarMovimento → WmsMovimentoSonda::registrarMovimento (status=confirmada, integracao=pendente)
    4. garantirMissaoEmAndamento (sem auto-conclusão; conclusão é explícita)
    5. registrarExtrato (auditoria)
        │
        ▼
INTEGRAÇÃO PUSH (assíncrona — worker)
  crons/wms_alocacao_integracao.php  (a cada ~2min)
    → varre wms_movimentos (status=confirmada AND integracao_* IN ('pendente','falha'))
    → Erp/WmsAlocacaoWmsHookStrategy::sincronizar
       → adapter->operacaoPush*()  (opc)  +  adapter->payloadPush*()  (com operacao_tipo)
       → marca integracao_erp/_wms = enviado | falha   (ERP e WMS independentes)
```

**Push-only:** o WMS externo é o dono do saldo oficial; o Genesis **não lê** o saldo
do WMS — só empurra os movimentos. O "saldo local" exibido
(`ConsultarEnderecoLaboratorio` / `WmsMovimentoSonda::saldoPorLocalizacaoProduto`) é a
**soma dos movimentos confirmados gravados aqui** (`fonte=local`), com aviso de que o
oficial é do WMS. Movimentos `cancelada` nunca são integrados (gate por status no cron
e em `buscarPendentesIntegracao*`).

---

## 5. Receita: como adicionar uma operação nova

Exemplo: criar `inventario_endereco`. (Para `inventario_produtos`, o passo 1 já está
feito em `InventarioProdutosAdapter.php` — falta do passo 2 em diante.)

1. **Enum** — garantir o case em `Enums/OperacaoTipo.php` e ajustar os predicados
   `exigeQuantidade/Endereco/Produto` (são dicas de fluxo p/ a view). Já existem os 5 cases.

2. **Adapter** — `Strategies/Operacao/InventarioEnderecoAdapter.php extends OperacaoAdapterBase`:
   - `getOperacaoTipo()` → o case do enum;
   - `validarOperacao($dados, $missao)` → regras próprias (campos obrigatórios, lookups);
   - `viewExecucao()` → `'html/operacaowms/inventario_endereco'` (a tela do passo 5);
   - `operacaoPushErp/Wms()` → nome do `opc` da operação (ex.: `registrar_contagem_wms`);
   - sobrescreva `montarMovimento`/`payloadPush*` **só se** o registro/payload fugir do padrão
     (ex.: `validacao_ocupacao` precisa de `ocupacao_estado` → sobrescreve montarMovimento).

3. **TddTest** — `Testes/Strategies/Operacao/InventarioEnderecoAdapterTddTest.php`
   (espelhe `InventarioProdutosAdapterTddTest`: identidade, predicados, push, validarOperacao
   rejeições+aceitações, montarMovimento, payloadPush). Roda em `dev_gx_goldie`, limpa resíduo.

4. **Registrar no factory** — 1 linha em `OperacaoAdapterFactory::$adapters`. A partir daqui
   a operação aparece em `operacoesSuportadas()` → o seletor da Designação a mostra sozinho e
   `getConfiguracoesAdicionais` passa a aceitá-la. **NÃO registre antes da tela existir**
   (senão dá pra criar uma missão que não tem como executar).

5. **Tela do operador** — `App/html/operacaowms/inventario_endereco.php` (+ `.js`). Use
   `operacaoTipo` + `precisaEndereco/Produto/Quantidade` que o Handler já injeta no viewData.
   Siga as convenções de `genesis-views-i18n` (strings via `show()` nas 3 línguas).

6. **(Se precisar) escopo na Designação** — a Strategy hoje seleciona PRODUTOS. Operações por
   ENDEREÇO podem exigir um escopo diferente (selecionar endereços) — ajuste
   `AlocacaoWmsStrategy::getPrimaryItems` / `GestaoMissoesAlocacaoWms::buscarItens` conforme a
   operação (idealmente delegando ao adapter num método de escopo, quando a 2ª operação por
   endereço chegar).

> **O que você NÃO toca:** `ConfirmarAlocacaoLaboratorio`, `ConfirmarAlocacaoValidator`
> (gate transversal), `AlocacaoWmsHandler` (dispatch genérico), os dois HookStrategy e o cron.
> Eles já são operation-agnostic.

---

## 6. Modelo de dados — `wms_movimentos`

Log **append-only** por tenant (`customerControl=true`; colunas `block_*` sem DEFAULT). Cada
confirmação é uma linha nova (nunca sobrescreve; o histórico permanece).

| Coluna | Notas |
|---|---|
| `id`, `missao_logistica_id`, `missao_item_id` (nullable — item dinâmico) | vínculo |
| `produto_id`, `localizacao_codigo`, `wms_localizacao_id` (nullable) | alvo |
| `quantidade` (nullable) | semântica POR OPERAÇÃO: alocação=delta opcional; inventário=contagem absoluta; validação=null |
| `operacao_tipo` (default `alocacao`) | ★ discriminador |
| `ocupacao_estado` (nullable) | só `validacao_ocupacao` |
| `status` | `confirmada` \| `cancelada` |
| `origem_leitura` | `codigo_barras` \| `manual` |
| `operador_id`, `confirmada_em` | auditoria |
| `integracao_erp`, `integracao_wms` (+ `_em`, `integracao_erro`) | estados de push independentes; índices parciais para retry |

> **Migração (deploy):** a `004` renomeia `wms_alocacoes`→`wms_movimentos` + colunas. É
> idempotente, mas **precisa rodar no prod junto com o código** (o código aponta para
> `wms_movimentos`). A `MigrationSchemaTest` aplica 001→004 e valida o schema.

---

## 7. Convenções e gotchas

- **Sondas V2** (`ModelInteligencia`): writes via `create()`/`update()` — **sem raw SQL** no
  código de produção (raw SQL só nos testes/CLI). `$required` como lista quebra o insert →
  validar no Validator/Adapter. `findById()` pode vir embrulhado em array (`$x = is_array($x) ? $x[0] : $x`).
- **Colunas `block_*`** são preenchidas pelo framework (customerControl) — declarar **sem DEFAULT**.
- **i18n**: strings via `show('texto PT literal')` (chave = md5); harvest automático; preencher EN/ES depois.
- **Constelações = só model/banco.** Os HookStrategy vivem em Constelacoes mas já referenciam
  Sondas/Adapter do sinal (acoplamento pré-existente, ok). Views ficam no sinal App.
- **Conclusão é explícita** (sessão livre não auto-conclui por contagem de itens).
- **i18n/cache**: `item.js` é incluído inline no `item.php` (sem cache-bust manual).

---

## 8. Testes

TddTests CLI (não-PHPUnit), espelhando a árvore, rodam **só em `dev_gx_goldie`** com bootstrap
de sessão em dois lugares (`$_SESSION['galaxiaUserInfo']` e `['dashboard']['galaxiaUserInfo']`).
Locations de teste usam prefixo `__test%`/`ZZ-%` e limpam no `finally`.

```bash
# rodar um teste
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/Operacao/AlocacaoAdapterTddTest.php

# integração (aplica migrations / fluxo fim-a-fim)
php tests/integration/AlocacaoWms/MigrationSchemaTest.php
php tests/integration/AlocacaoWms/AlocacaoFluxoTest.php
```

Baseline atual: 11 suítes, ~244 asserts, 0 falha. **Sempre rode a suíte antes/depois** de mexer
em algo do caminho da alocação — ela é a rede de não-regressão.

---

## 9. Status (refatoração `operacao-wms`)

| Fase | O quê | Estado |
|---|---|---|
| F1 | `wms_movimentos` (rename + `operacao_tipo`/`ocupacao_estado`), Sonda `WmsMovimentoSonda`, enum `OperacaoTipo` | ✅ |
| F2 | `OperacaoAdapter` (Interface/Base/Factory) + `AlocacaoAdapter`; Validator/Laboratório delegam | ✅ |
| F3 | Criação persiste `operacao_tipo` (gated); Strategy expõe operações suportadas; form/JS prontos | ✅ (seletor visível adiado) |
| F4 | Handler despacha por `viewExecucao()` do adapter | ✅ |
| F5 | Push: opc + payload via adapter (com `operacao_tipo`); cron passa o tipo | ✅ (preparado; WMS externo não conectado) |
| F6 | Operações novas | `inventario_produtos` ✅ e `inventario_endereco` ✅ (usáveis ponta a ponta; contagem compartilha `InventarioAdapterBase`); `conferencia_localizacao`, `validacao_ocupacao` ⏳ |

**Alocação roda em produção com paridade total** durante toda a refatoração. As fases
F1–F6 estão validadas **em dev**; o deploy precisa aplicar a migration `004`.

---

## 10. Extensão: busca manual + ocorrência "sem código de barras"

Spec/plano: `PRD/operacao-wms/10-busca-manual-ocorrencia-codigo-barras-spec.md` e `11-...-plan.md`.

Quando o produto não tem código de barras pra bipar, a execução da alocação (`alocacao`)
ganha dois recursos:

1. **Busca manual por descrição** — `BuscarProdutoLaboratorio::buscar($termo)` (read-only;
   `descricao`/`codigo_produto` via `ILIKE`, escopo do tenant pelo `customerControl`).
   Ação do dispatcher: `buscarProduto`. No front, selecionar um resultado segue o **mesmo
   caminho** da bipagem (`_stagearProduto`), sem duplicar lógica.
   > Gotcha: o `%` do `LIKE` precisa de `urlencode()` no param-string (o `parse_str` do
   > `ModelInteligencia` interpreta `%` como escape de URL); usar `:t`/`:t2` distintos.

2. **Ocorrência de código de barras** — reusa o sinal `SugestoesCodigoBarras`
   (tabela `missao_codigo_sugestao`), com a coluna `tipo` discriminando o motivo
   (operador só sinaliza, **não digita código**):
   - **`tipo='ausencia'`** — produto **sem** `codigo_barras` (atribuir um depois).
   - **`tipo='codigo_errado'`** — produto **com** `codigo_barras`, mas o código está
     errado no cadastro (corrigir depois). O código atual vai no snapshot do item
     (`dados_adicionais.codigo_barras`) como contexto.

   O front mostra o botão adaptado ao produto ("Sinalizar sem código de barras" vs
   "Sinalizar código de barras errado") e envia o `tipo`; o Validator bifurca a
   pré-condição (`codigo_errado` exige código; `ausencia`/`sugestao` proíbem). Ações
   do dispatcher: `sinalizarSemCodigo` (aceita `tipo`) / `cancelarSemCodigo`. Caminho:
   - `SinalizarSemCodigoLaboratorio` → `SugerirCodigoBarrasValidator` (ramo `ausencia`:
     mantém "missão ativa + produto sem `codigo_barras`", pula regras de código) →
     `MissaoCodigoSugestaoSonda::registrarAusencia` (idempotente por missão+produto).
   - **Ao concluir a missão de alocação** (hook em `MissoesStatus::transicionar`, agora
     com `ALOCACAO_WMS` na whitelist), `GerarAjusteDeSugestoesLaboratorio` gera **1**
     missão `AJUSTE_DADOS` **executável** — `status='pendente'`, itens **pendentes**,
     `campos_solicitados=['codigo_barras']` — pra alguém **atribuir** o código depois.
     Idempotente (`jaTemAjusteParaOrigem`).

> **Diferença-chave** do fluxo legado `sugestao` (separação/carregamento, com código): lá a
> missão de ajuste nasce **concluída + aguardando aprovação** e a aprovação **grava** o código
> sugerido. No `ausencia` não há valor a aplicar, então a missão nasce como **trabalho a
> executar**. O ramo `sugestao` permanece intacto.

Migration: `SugestoesCodigoBarras/Migrations/002_suporte_ausencia_codigo.sql`
(`codigo_sugerido` NULLable + coluna `tipo`). Testes em `SugestoesCodigoBarras/Testes/...`
e `tests/integration/AlocacaoWms/OcorrenciaSemCodigoFluxoTest.php`. Smoke do front (busca +
sinalizar/desfazer) é manual no browser.
