# 03 - Regras de Negócio

> As regras implícitas extraídas do código — com **por que existem**, que problema resolvem e o que acontece se forem alteradas. Este é o capítulo que protege as decisões de negócio de refactors bem-intencionados.
>
> Relacionados: [02 - Fluxos](02-fluxos-do-sistema.md) (onde cada regra atua), [12 - Armadilhas](12-armadilhas-conhecidas.md) (consequências técnicas).

## 1. Identidade e dados compartilhados com o ERP

### R1. Cruzamento Genesis ↔ ERP é sempre por `importacao_id` = `codigoMD5`
- **Por quê:** `numero_pedido` é reutilizado entre empresas e anos no ERP; qualquer join por ele mistura pedidos de clientes diferentes.
- **Impacto se alterar:** joins/guards por `numero_pedido` produzem atualizações no pedido errado — bug silencioso de dados.
- **Onde:** `PedidoErpSonda` (chave `(codigoMD5, block)`), hooks de alocação WMS (payload leva `importacao_id`), validação de identidade da IntegracaoWms.

### R2. Timestamps do ERP são UTC; exibição é America/São_Paulo
- **Por quê:** o ERP grava UTC; o usuário opera em horário de Brasília.
- **Impacto:** exibir cru = 3h de erro em todos os relatórios de expedição.

### R3. `pedidos.comodato` (ERP) é NULL em quase todos os registros
- **Por quê:** a coluna foi adicionada tarde; nunca houve backfill.
- **Regra:** filtros usam `COALESCE(comodato, 0)`. Um `comodato = 0` puro elimina praticamente todos os pedidos.

## 2. Missões logísticas

### R4. Vários operadores na mesma missão é intencional
- **Por quê:** uma separação grande é feita em paralelo por vários operadores; travar a missão em um operador criaria gargalo físico no armazém.
- **Como:** a exclusão mútua acontece **por item**, via `sp_reservar_item_da_lista` com `FOR UPDATE SKIP LOCKED` (operadores diferentes nunca disputam o mesmo item) + `pg_advisory_xact_lock(usuario)` (duplo clique do mesmo operador não duplica).
- **Impacto se alterar:** qualquer "lock de missão por operador" quebra o modelo operacional; qualquer remoção do SKIP LOCKED reintroduz corrida de item duplicado.
- **Complemento:** vínculo de equipe (`missao_logistica_usuarios`) nunca é deletado — saída de operador marca `saiu_em` (auditoria preservada, vínculo reativável).

### R5. Stickiness de item: operador com item em andamento recebe o mesmo item
- **Por quê:** evitar que o operador "perca" o item ao recarregar a tela/app no meio da coleta.
- **Onde:** `MissoesExecucao::obterProximoItem` verifica primeiro se o usuário já tem item `em_andamento`.

### R6. Transições de status passam por `MissoesStatus::transicionar()` — e só por ele
- **Por quê:** é o único ponto que valida a máquina de estados (`StatusMissao::proximosStatusPossiveis()`) **e** dispara os efeitos em cascata: separação concluída sem conferência gera carregamento; carregamento concluído finaliza o pedido; conclusão sincroniza `romaneios.separado/carregado`; sugestões de código viram missão de ajuste.
- **Impacto se alterar:** um `update()` direto no status pula os hooks — pedidos ficam "presos" (carregados mas não finalizados), missões seguintes não nascem.
- **Nota:** hooks usam `class_exists()` + try/catch não-fatal — a transição não é desfeita se um hook falhar (decisão consciente: operação > consistência de efeitos secundários).

### R7. `cancelada → pendente` é transição válida (reabertura)
- **Por quê:** gestor cancela por engano; recriar a missão perderia o histórico e os vínculos.

### R8. Corte logístico: finalizar manualmente remove itens sem bipagem e pode regerar missão
- **Por quê:** quando falta produto, a missão não pode ficar aberta para sempre; o saldo cortado vira nova missão quando o estoque volta.
- **Onde:** `finalizarManualmente` (itens → `REMOVIDO`; regeneração opcional pelo saldo).

### R9. Missão de retorno sempre exige aprovação (`requer_aprovacao=true`)
- **Por quê:** retorno mexe em estoque e em pedidos já faturados — o gestor precisa validar o que voltou antes de o sistema aceitar.
- **Contraste:** missões vindas de ordem WMS externa nascem com `requer_aprovacao=0` (R15) — são contextos diferentes, não inconsistência.

## 3. Bipagem e código de barras

### R10. O fator de bipagem é por código de barras, não por produto
- **Por quê:** o mesmo produto tem EAN unitário (fator 1) e DUN de caixa (fator 10+). O código lido diz quantas unidades entram.
- **Fonte da verdade:** `produtos_codigos_barras.fator` — consultada **antes** dos campos do produto; o fallback pelos campos do produto usa fator 1.
- **Impacto se alterar a precedência:** caixas passam a contar como 1 unidade (ou vice-versa) sem nenhum erro visível.

### R11. Validação ampla só aceita o `codigo_barras` principal
- **Por quê:** aceitar `id`/`codigo_alternativo`/`codigo_produto` no scan livre poderia colidir com o EAN real de outro produto — o operador biparia o produto errado "com sucesso".

### R12. Itens pesáveis são isentos de teto e de fator de caixa
- **Por quê:** peso varia (KG/G/MT/L); o teto de quantidade bloquearia pesagens legítimas.
- **Para os demais:** exceder o limite **bloqueia o incremento inteiro** (nada é somado) — evita "estourar um pouquinho" repetidamente.

### R13. Retorno não valida código de barras
- **Por quê:** mercadoria devolvida volta como está (avariada, sem etiqueta); exigir bipagem travaria o fluxo de retorno.

### R14. Ajuste manual (+/-) é sempre unitário
- **Por quê:** o fator pertence à **leitura** do código; o ajuste manual é correção fina do operador.

## 4. WMS e integração externa

### R15. Ordem de WMS externo nasce com `requer_aprovacao = 0`
- **Por quê:** o gate de aprovação retém o push de movimentos; com aprovação ligada, a ordem nunca chegaria a `reportada` e o ciclo com o WMS não fecharia.
- **Impacto se alterar:** ordens externas ficam eternamente abertas no WMS do parceiro.

### R16. O WMS externo é o dono do saldo (push-only)
- **Por quê:** dois sistemas escrevendo saldo = divergência garantida. O Genesis registra movimentos e **empurra**; nunca lê saldo de lá. O "saldo local" (soma de movimentos confirmados) é exibido com aviso.
- **Seam futuro:** `EsperadoProviderRegistry` do inventário cíclico é o ponto para um dia consultar o WMS sem mudar o fluxo.

### R17. Dedup por `external_ref`; identidade desconhecida rejeita a ordem inteira
- **Por quê:** reenvio de webhook é normal (retry do parceiro) — deve devolver o veredito original, nunca duplicar missão. E uma ordem parcialmente traduzida (produto conhecido, endereço não) criaria missão meio-pronta impossível de executar — rejeição total com `motivo` persistido é auditável e reprocessável.

### R18. Tenant do webhook é fail-closed
- **Por quê:** o payload externo não é confiável para decidir empresa. A fonte é a linha autenticada do canal (`webhook_endpoints.block_empresa_id`); divergência com a URL → rejeita.

### R19. Movimento WMS é append-only; cancelamento é estorno, não delete
- **Por quê:** o movimento pode já ter sido integrado ao ERP/WMS externo — apagar localmente criaria divergência. Estados `estorno_pendente/estornado/estorno_falha` propagam o desfazimento.
- **Complemento:** integração ERP e WMS têm flags **independentes** (`integracao_erp`, `integracao_wms`) — um lado pode falhar e ser retentado sem re-enviar o outro.

### R20. Item de missão WMS é vínculo dinâmico, sem auto-conclusão
- **Por quê:** alocação é "sessão livre" — o operador vai alocando o que chega; o item (missão, produto) nasce no primeiro vínculo e a missão só fecha quando o operador encerra explicitamente.

## 5. Edição de rota

### R21. Motorista acessa por token público + OTP, nunca por login
- **Por quê:** motorista é terceirizado/rotativo — criar usuário Galaxia para cada um é inviável. Token de 64 hex + OTP por WhatsApp dá identidade suficiente para a operação.
- **Timer:** a sessão expira (`expira_em`, default 30 min) para o link não ficar editável indefinidamente.

### R22. Aprovação de edição é idempotente
- **Por quê:** o gestor pode clicar duas vezes / o polling pode reenviar. Aprovar algo já aprovado retorna sucesso **sem re-disparar callbacks** (senão a sequência seria reaplicada e eventos duplicados).

### R23. `rota_original` só é gravada se estiver NULL
- **Por quê:** preservar a rota original de antes da **primeira** edição — em edições sucessivas, sobrescrever apagaria o baseline de comparação.

### R24. A resposta da aprovação carrega o payload de refresh (não confia no socket)
- **Por quê:** o publish do Scaledrone via REST já falhou em produção (HTTP 500) e o realtime morreu em silêncio. Regra geral do sistema: **write-before-fire** — o negócio persiste e a resposta HTTP carrega o necessário; socket é bônus.

### R25. Rejeição é no-op no domínio
- **Por quê:** rejeitar só marca o status; a rota vigente permanece. Não há rollback a fazer.

## 6. Lotes e staging

### R26. A lista de um lote é imutável
- **Por quê:** o lote é uma fila auditável — editar a lista depois de criada quebraria a rastreabilidade do que entrou e por quê. Só se consome/conclui.
- **Complemento:** inserção é idempotente ("já pendente neste lote" = sucesso) porque os produtores (retorno, faltas) reprocessam.

### R27. Criar missão de lote passa por verificador de idempotência
- **Por quê:** o mesmo lote não pode gerar duas missões (duplicaria trabalho físico).

## 7. Multi-tenant e exclusão

### R28. Exclusão é sempre soft delete (`block = 0`)
- **Por quê:** auditoria, recuperação e integrações que referenciam ids antigos. O sistema inteiro assume isso.
- **Impacto se alterar:** um `DELETE` físico quebra referências no ERP/WMS e apaga histórico legal.

### R29. Cliente 7 é o tenant "global"
- **Por quê:** registros compartilhados entre todos os clientes (catálogos, configurações padrão) vivem com `block = 7`; o filtro de leitura sempre inclui `OR block = 7`.
- **Impacto:** gravar dado de cliente com `block = 7` vaza para todos os tenants.

### R30. Colunas `block_*` sem DEFAULT nas migrations
- **Por quê:** o valor **deve** vir da sessão via framework; um `DEFAULT 1` mascararia sessão quebrada gravando tenant errado silenciosamente.

## 8. OTP e mensageria

### R31. Config de OTP é por contexto
- **Por quê:** cada fluxo tem exigência própria (edição de rota: 300s, 3 tentativas, 6 dígitos). Um default global forçaria o denominador comum.

### R32. Canal `exibir_tela` existe para operação assistida
- **Por quê:** motorista sem WhatsApp/sinal — o gestor vê o código na tela e dita por telefone. Por isso dois endpoints devolvem código em claro **por design** (o audit log redacta).

### R33. Fallback de canal é o stub de log, nunca silêncio
- **Por quê:** em dev (ou config incompleta), o envio "funciona" logando — o fluxo é testável sem provedor real. O bootstrap registra provedores reais primeiro e o stub só onde faltou.

## 9. Metas e comissões (módulo de vendas)

### R34. Bônus de meta V2 fica em `bonificacao_config.por_alvo`
- **Por quê:** o modelo V2 paga por alvo atingido (JSON estruturado); o campo flat `bonificacao_valor` permanece 0 por compatibilidade. Cálculo canônico: `RComissaov2::bonusMetaV2ParaVendedor()`.
- **Impacto:** ler o campo flat = bônus sempre zero.

## 10. Dashboards

### R35. Dashboard é cache-first
- **Por quê:** as queries de KPI são caras; o cron diário materializa e os endpoints leem o cache. Tempo real não é requisito — atraso de até 1 dia é aceito de negócio (dados do dia corrente vêm de endpoints específicos).
- **Padrão `?light=1`:** primeiro paint só com números; séries pesadas em background.

### R36. Período padrão é a semana ISO anterior
- **Por quê:** o gestor analisa a semana fechada. Persistência em `sessionStorage` (morre com a aba); querystring vence sobre tudo (links compartilháveis).

## 11. Processo (regras da equipe)

- **R37.** Código novo usa **apenas** padrões V2 (Sondas, dispatcher fino, Validators). Inteligências V1 são leitura/manutenção.
- **R38.** `Constelacoes/` aceita **somente models** — views/HTML de qualquer feature vivem no Sinal.
- **R39.** Writes em lote usam `update()` do model — nunca raw SQL (preserva tenant, auditoria, soft delete).
- **R40.** Commits/pushes só com pedido explícito; trabalho em `dev_team`; sem merge para `main`.
