# Validação automática de código de barras + fator na bipagem — Plano técnico

> Entrada: `PRD/bipagem-codigo-barras-fator/01-spec.md`. Decisões de produto TRAVADAS (validação sempre ligada; manual = só +/- unitário; fator que excede bloqueia; resolução central na Sonda; código morto fora de escopo; migration versionada idempotente). Este plano não reabre nenhuma delas — apenas define o "como".

## Resumo de decisão arquitetural

Não há Sinal novo, Adapter novo, nem Registry novo. A feature é uma **extensão cirúrgica** de um fluxo existente (Missão Logística, sinal `App`, separação e carregamento), apoiada numa **Sonda V2 já existente** (`ProdutoCodigoBarraSonda`) à qual adicionamos **dois métodos de leitura**. O padrão V2 dominante aqui é **Sonda como fonte da verdade + Presenter montando o contrato pro front + Satélite como autoridade no write** — exatamente o que `Otp` (Sonda canônica `find()->order()->fetch()`) e o sincronizador `Produtos/Sincronizacoes/CodigosBarras` (writes/reads V2 com `turnOff`/`propetiesCustomized`) já praticam. **Não introduzimos Strategy/Registry**: há um único fluxo de resolução de código (campos do produto + `produtos_codigos_barras`), então qualquer abstração de pluginização seria padrão prematuro (a spec não pede múltiplos resolvedores nem múltiplos consumidores externos — diferente de `Edicaorota/Callbacks/Registry`, que existe porque vários módulos consomem o evento de finalização).

O fator é **aplicado no frontend** (UX imediata + offline) e **validado/autoritativo no backend** (online), reaproveitando a infra parcial de `codeMap` (front) e o teto de `validarLimites` (back). O "1 leitura = N unidades, 1 log" cai naturalmente de uma única chamada de incremento com `delta = fator`.

## Sinal de referência

- **Imitar (Sonda — métodos de leitura):** `componentes/Missoes/Mensageria/sinais/Otp/Sondas/OtpVerificacaoSonda.php:67-94` — padrão `find($criterios, $params)->order('id DESC')->fetch($bool)`, `fetch(false)` → objeto único, `fetch(true)` → array. É a referência canônica V2 (memória `feedback_referencias_v2`).
- **Imitar (reads/writes V2 da própria tabela + gotchas):** `componentes/Missoes/Produtos/sinais/Sincronizacoes/Laboratorios/CodigosBarras.php:92-103,150-193` — uso de `turnOff(['customerControl' => false])`, `propetiesCustomized(['customerControlId' => $block])`, e o gotcha do `data()`/`findById()` vir embrulhado em `[obj]`.
- **Imitar (migration idempotente versionada por sinal/constelação):** `componentes/Missoes/Mensageria/sinais/Otp/Migrations/001_initial_schema.sql` e `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Migrations/001_create_missao_codigo_sugestao.sql` (ambas PostgreSQL).
- **Não imitar:** a Inteligência V1 `GalaxiaConstelacoes\Produtos\CodigosBarra\Inteligencias\ProdutoCodigoBarra` (singular, tabela inexistente) e `CodigosBarra/sql/produto_codigo_barra.sql` — **código morto, fora de escopo, não tocar**. Também não imitar Designacoes/Pessoas (legado V1).

## Estrutura proposta

### Novos arquivos
```
componentes/Constelacoes/Produtos/CodigosBarra/
└── Migrations/
    └── 001_create_produtos_codigos_barras.sql      # DDL idempotente PostgreSQL alinhado ao schema real

componentes/Constelacoes/Logistica/MissaoLogistica/Testes/
└── Laboratorios/
    └── FatorBipagemIntegrationTest.php             # TDD/integração contra dev_gx_goldie (Sonda + resolução)
```

### Arquivos alterados
```
componentes/Constelacoes/Produtos/CodigosBarra/Sondas/ProdutoCodigoBarraSonda.php   # +resolverPorCodigo(), +listarPorProdutos()
componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesBipagem.php     # resolverProduto()+biparItem()+payload extrato
componentes/Constelacoes/Logistica/MissaoLogistica/Presenters/MissaoPresenter.php   # montar codigosBarras por item (1 query/missão)
componentes/Constelacoes/Logistica/MissaoLogistica/Presenters/SeparacaoPresenter.php # idem (sobrescreve por item)
componentes/Constelacoes/Logistica/MissaoLogistica/Presenters/PesagemPresenter.php   # idem
componentes/Missoes/Logistica/sinais/App/html/shared/components/v2/execucao_bipagem.php # preservar codigosBarras do itemBase
componentes/Missoes/Logistica/sinais/App/html/separacao/item.js                     # incremento item-selecionado via codeMap; validar por _codigosSet
componentes/Missoes/Logistica/sinais/App/html/carregamento/item.js                  # incremento item-selecionado via codeMap; validarCodigo default true
linguagens/pt-br.json, linguagens/en.json, linguagens/es.json                       # strings de bloqueio/erro (ver §i18n)
```

> **Sem rota nova.** A bipagem (separação/carregamento) não passa por `/api/v1`; é dispatchada pelo Satélite `MissoesBipagem` via Galáxia. Nenhuma alteração em `Plataforma/sinais/Api/Routes.php`.

---

## 1. Sonda — `ProdutoCodigoBarraSonda` (2 métodos novos de leitura)

Arquivo: `componentes/Constelacoes/Produtos/CodigosBarra/Sondas/ProdutoCodigoBarraSonda.php`. A Sonda já tem `customerControl => true` e `db => 'gx_goldie'` (`:54-60`). Os dois métodos novos seguem o padrão `OtpVerificacaoSonda` (`find()->fetch()`), **sem raw SQL**, deixando o `customerControl` injetar o filtro de tenant automaticamente.

### `resolverPorCodigo(string $codigo, ?int $produtoId = null): ?object`

**Objetivo:** dado um código bipado (e, quando conhecido, o `produto_id` esperado), retornar o registro de `produtos_codigos_barras` correspondente **no tenant da sessão**, ou `null`.

**Comportamento:**
- Normaliza: `$codigo = trim($codigo)`. Se vazio → `return null`.
- Critério base: `codigo_barras = :c`. Se `$produtoId` informado, adiciona `AND produto_id = :p` (resolução estrita; garante o registro do produto esperado).
- `customerControl` **ligado** (default) → o WHERE recebe automaticamente `(block = :sessão OR block = 7)`. O `7` é o tenant compartilhado/global do framework (visto em `ModelInteligencia::customerQuery`), então códigos globais continuam resolvendo — comportamento desejado e consistente com o resto do sistema. **Não desligar** `customerControl` aqui (diferente do sincronizador, que cruza tenant via webhook): na bipagem o tenant correto é o da sessão (AC1.5).
- Ordenação determinística: `->order('produto_id = ' ... )` não é necessário; usar `->order('id ASC')` para resultado estável caso haja duplicata histórica.
- Retorno: `fetch(false)` (objeto único) → `?: null`.

**Assinatura/contrato de retorno:** o objeto traz pelo menos `id, produto_id, codigo_barras, tipo_codigo_barras, fator`. O **fator** vem como `numeric(6,2)` (string `"5.00"` no PDO pgsql) — quem consome deve `floatval()`.

```
// pseudo (não é o código final)
public function resolverPorCodigo(string $codigo, ?int $produtoId = null): ?object {
    $codigo = trim($codigo);
    if ($codigo === '') return null;
    $criterios = "codigo_barras = :c";
    $params    = "c=" . urlencode($codigo);
    if ($produtoId) { $criterios .= " AND produto_id = :p"; $params .= "&p=" . (int)$produtoId; }
    $r = $this->find($criterios, $params)->order('id ASC')->fetch(false);
    return $r ?: null;
}
```

### `listarPorProdutos(array $produtoIds): array`

**Objetivo:** uma única query por missão para os Presenters montarem `codigosBarras` por item, **evitando N+1**.

**Comportamento:**
- Sanear IDs: `array_values(array_unique(array_filter(array_map('intval', $produtoIds))))`. Se vazio → `return []`.
- `WHERE produto_id IN (id1,id2,...)` — IDs já inteiros, interpolados direto na cláusula `IN` (mesmo padrão do batch da skill `genesis-sondas-db`: `update(..., 'id IN (' . implode(',', $ids) . ')')`). Sem placeholders para a lista (PDO não expande arrays).
- `customerControl` ligado → filtro de tenant automático.
- `fetch(true)` → array de objetos.
- **Forma de retorno:** agrupado por `produto_id` para consumo direto no Presenter:
  `[ produto_id => [ ['codigo_barras' => ..., 'fator' => floatval(...), 'tipo_codigo_barras' => ...], ... ] ]`.
  O agrupamento é feito em PHP após o `fetch(true)`.

```
// pseudo
public function listarPorProdutos(array $produtoIds): array {
    $ids = array_values(array_unique(array_filter(array_map('intval', $produtoIds))));
    if (!$ids) return [];
    $r = $this->find("produto_id IN (" . implode(',', $ids) . ")", null)->fetch(true);
    $r = is_array($r) ? $r : ($r ? [$r] : []);
    $map = [];
    foreach ($r as $row) {
        $map[(int)$row->produto_id][] = (object)[
            'codigo_barras'      => trim($row->codigo_barras),
            'fator'              => floatval($row->fator),
            'tipo_codigo_barras' => $row->tipo_codigo_barras ?? 'proprio',
        ];
    }
    return $map;
}
```

**Precedência do fator (regra de negócio §1 da spec):** quando um código existe tanto num campo do produto (fator 1) quanto em `produtos_codigos_barras`, a **tabela vence**. Isso é garantido na **ordem de montagem**: tanto no front (`codeMap`) quanto no back (`resolverProduto`), os códigos da Sonda são aplicados **por último/com prioridade** sobre os campos do produto (detalhado em §3 e §4). A Sonda em si não precisa resolver precedência — ela só devolve a verdade da tabela.

**Gotchas respeitados:** `$required = []` (já está, `:52`); nenhum write nestes métodos (só leitura), então `create()`/`data()` embrulhado não se aplica aqui; os métodos retornam direto de `fetch()`.

---

## 2. Migration versionada

**Caminho:** `componentes/Constelacoes/Produtos/CodigosBarra/Migrations/001_create_produtos_codigos_barras.sql`

**Justificativa do local:** `CodigosBarra/` é uma **Constelação** (só model/banco — memória `feedback_constelacoes_sem_html`); a tabela é compartilhada (sincronizador + bipagem). A migration mora junto da Sonda que a representa, no padrão `Migrations/NNN_*.sql` (memória: migrations nunca em `db/`). Não vai dentro de um sinal porque não pertence a um sinal específico.

**Driver:** PostgreSQL (confirmado: `dev_gx_goldie` é pgsql). DDL **idempotente** (`CREATE TABLE IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS`) porque a tabela **já existe no dev** (populada pelo ERP) — a migration documenta/versiona o schema real e é segura de re-rodar e de aplicar em ambientes onde a tabela ainda não existe.

**DDL alinhado ao schema real (capturado do banco):**

```sql
-- =====================================================
-- Migration: produtos_codigos_barras (Constelacao Produtos/CodigosBarra)
-- Fonte da verdade do `fator` por código de barras de produto.
-- Populada pelo ERP via Produtos/Sincronizacoes/CodigosBarras e lida na bipagem.
-- Idempotente: a tabela já existe em ambientes com ERP sincronizado.
-- block é a coluna de tenant (customerControl). block_* auxiliares são
-- preenchidas pelo framework — sem DEFAULT (padrao Galaxia).
-- =====================================================

CREATE TABLE IF NOT EXISTS produtos_codigos_barras (
    id                  BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,

    -- refs ERP
    codigomd5           VARCHAR(100),
    produto_codigomd5   VARCHAR(100),

    -- dominio
    produto_id          BIGINT       NOT NULL,
    codigo_barras       VARCHAR(60)  NOT NULL,
    tipo_codigo_barras  VARCHAR(20)  NOT NULL DEFAULT 'proprio',
    fator               NUMERIC(6,2) NOT NULL DEFAULT 1,

    -- auditoria
    data_cadastro       TIMESTAMP WITHOUT TIME ZONE,
    data_alteracao      TIMESTAMP WITHOUT TIME ZONE,
    usuario_cadastro    VARCHAR DEFAULT 'default',
    usuario_atualizacao VARCHAR DEFAULT 'default',

    -- tenant (obrigatorias, sem DEFAULT 1 — framework injeta via customerControl)
    block               INTEGER NOT NULL,
    block_usuario_id    INTEGER,
    block_filial_id     INTEGER,
    block_empresa_id    INTEGER,
    block_setor_id      INTEGER
);

-- Indices (espelham os reais; cobrem as 2 queries de leitura da bipagem)
CREATE INDEX IF NOT EXISTS idx_pcb_produto_block ON produtos_codigos_barras (produto_id, block);
CREATE INDEX IF NOT EXISTS idx_pcb_codigo_block  ON produtos_codigos_barras (codigo_barras, block);
CREATE INDEX IF NOT EXISTS idx_pcb_codigomd5     ON produtos_codigos_barras (codigomd5);
CREATE INDEX IF NOT EXISTS idx_pcb_tipo          ON produtos_codigos_barras (tipo_codigo_barras);
```

**Notas:**
- O schema real usa `id BIGINT` com `nextval('..._id_seq')`. Em DDL idempotente novo, `GENERATED BY DEFAULT AS IDENTITY` é o equivalente moderno; **se a paridade exata de sequence importar para o time de DBA**, trocar por `BIGSERIAL`. Como a tabela já existe no dev, o `IF NOT EXISTS` torna esse ponto inócuo lá; ele só rege ambientes novos. **Decisão:** usar `BIGSERIAL` para máxima fidelidade ao `nextval` existente (item de revisão abaixo).
- `idx_pcb_produto_block` cobre `listarPorProdutos` (filtra por `produto_id` + `block` do customerControl). `idx_pcb_codigo_block` cobre `resolverPorCodigo` (filtra por `codigo_barras` + `block`). **Nenhum índice novo é necessário** — a feature já está coberta.
- `block_*` auxiliares ficam nullable como no schema real (o framework preenche o que usa; `block` é o NOT NULL crítico).

---

## 3. Backend — `MissoesBipagem` (resolução + fator + extrato)

Arquivo: `componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesBipagem.php`.

### 3.1 `resolverProduto()` passa a devolver **produto + fator** (`:115-143`)

Hoje retorna `$produto` (objeto) ou um erro renderizado. Passa a retornar, no caminho de sucesso, um **par** `{ produto, fator, tipo_codigo_barras }`. Para não quebrar `identificarItemValido()` (`:102-113`, que só usa o produto para achar o item), a forma mais limpa é:

- **Propagação do fator via o objeto que já flui (decisão do solicitante — sem estado mutável no Satélite).** `resolverProduto()` continua retornando o **objeto produto** e anexa o fator **daquela leitura** (o fator é por-código, não por-produto): `$produto->_fator_bipagem` e `$produto->_tipo_codigo_barras`. Em seguida `buscarItemDaMissao()` **propaga essas duas propriedades para o item retornado** (`$itemMissao->_fator_bipagem`, `$itemMissao->_tipo_codigo_barras`), de modo que `biparItem` lê o fator direto do item resolvido. Nenhuma propriedade de instância do Satélite é usada — o fator viaja com o objeto da própria chamada.

**Ordem de tentativa (precedência tabela > produto), resolvendo a dúvida da spec "origem do fator na resolução estrita":**

1. **Caminho estrito (`$targetProdutoId` informado — luz dinâmica / item selecionado no front):**
   - Primeiro tenta a Sonda: `(new ProdutoCodigoBarraSonda())->resolverPorCodigo($codigo, $targetProdutoId)`.
     - Se achou → `produto = findById($targetProdutoId)`, `produto->_fator_bipagem = floatval($sonda->fator)`, `produto->_tipo_codigo_barras = $sonda->tipo_codigo_barras`. **return produto** (fator da tabela vence).
   - Se a Sonda não achou, mantém a regra atual `codigoPertenceAoProduto($codigo, $p)` (`:176-182`): se bate por campo do produto → `produto->_fator_bipagem = 1.0`, `_tipo_codigo_barras = 'proprio'`. **return produto**.
   - Se nem Sonda nem campo do produto batem, mas o produto existe → mesmo erro atual `"Código incorreto! ... não pertence a ..."` (AC1.4/divergência preservada).

2. **Caminho amplo (sem `$targetProdutoId` — scan sem item selecionado):**
   - Primeiro tenta a Sonda: `resolverPorCodigo($codigo)` (sem produtoId). Se achou → `produto = findById($sonda->produto_id)`, `_fator_bipagem = floatval($sonda->fator)`. **return produto**.
   - Se a Sonda não achou, mantém o `findAny` atual nos 4 campos do produto (`:134`) → fator 1.
   - Se nada acha → erro atual `"Código inválido: Produto não cadastrado"` (AC1.3 preservado).

> Importante: a Sonda é consultada **antes** dos campos do produto nos dois caminhos → garante a **precedência do fator da tabela** (regra §1). Quando a tabela não tem o código, cai no comportamento legado (fator 1) → **zero regressão** (regra §9, AC2.2).

### 3.2 `biparItem()` aplica o **fator** como delta (`:61-92`)

- Após `identificarItemValido()` devolver o item, o fator da leitura já está anexado nele (via §3.1). `biparItem` lê direto do item:
  - `$fator = floatval($itemEncontrado->_fator_bipagem ?? 1);`
  - `$tipo  = $itemEncontrado->_tipo_codigo_barras ?? null;`
  - Trocar o `1` fixo do `:91` por: `return $this->alterarQuantidade($itemEncontrado->id, $fator, $metodoEntrada, $codigo, $tipo);`
- **Estender a assinatura** de `alterarQuantidade($itemId, $delta, $metodoEntrada = 'scan', $codigoBipado = null, $tipoCodigoBarras = null)` e repassar `$tipoCodigoBarras` a `atualizarItemNoBanco` (parâmetro opcional no fim → compatível com as chamadas legado `adicionarQuantidade`/`reduzirQuantidade`).
- **`alterarQuantidade` não muda** (`:206-224`): já aceita `$delta` arbitrário, já chama `validarLimites` e grava 1 extrato. Passar `$fator` como delta produz naturalmente "N unidades, 1 log" (US2, US3, AC3.1).

> O fator **só** entra em `biparItem` (scan). `adicionarQuantidade`/`reduzirQuantidade` (`:187-195`, botões +/-) continuam com delta `±1` fixo → **manual permanece unitário** (US5, AC5.1, regra §5). Decisão TRAVADA respeitada: o fator não toca o ajuste manual.

### 3.3 Bloqueio quando o fator excede o pedido (US4)

- **Nenhuma mudança em `validarLimites` (`:226-246`).** Ele já compara `novaQtd > limiteReal` e **bloqueia** (retorna erro, nada é gravado) para itens não pesáveis; pesáveis (KG/G/MT/L/...) são isentos via `isUnidadePeso` (`:733-737`). Como `novaQtd = qtdAtual + fator`, uma caixa que estoura o pedido é barrada e **nada é somado** (AC4.1, AC4.3). O operador completa depois com unitário/manual (AC4.4).
- O feedback de erro de teto já existe (mensagem `"Quantidade excedida! Limite: ..."`). Ver §i18n para padronizar a string.

### 3.4 Enriquecer o payload do extrato (AC3.2) — `atualizarItemNoBanco` (`:248-297`)

- O payload (`:285-296`) ganha 2 chaves: `'fator'` e `'tipo_codigo_barras'`.
- `atualizarItemNoBanco` recebe `$tipoCodigoBarras` pela assinatura estendida (§3.2). Como o `$delta` que chega **é** o fator da leitura:
  - `'fator' => abs(floatval($delta))` (no scan = fator; no manual, delta=±1 → fator 1).
  - `'tipo_codigo_barras' => $tipoCodigoBarras` (null no manual).
- A ação do extrato **não muda**: `resolverAcaoExtrato` (`:323-338`) continua mapeando `scan → BIPAGEM_SCAN`, `manual → BIPAGEM_MANUAL` (AC3.3). **Nenhum case novo no enum `AcaoExtrato`** — o enum já cobre tudo (verificado).

**Chaves finais do payload do extrato (resolve a dúvida da spec):** `fator` (float) e `tipo_codigo_barras` (string|null), adicionadas ao payload existente sem renomear nada. Mantêm `delta`, `codigo_bipado`, `metodo_entrada`.

### 3.5 `bipagemLote` / `processarLoteDeltas` (offline) — confirmação, **sem mudança de lógica** (`:404-486`)

- O lote **confia no `quantidade_delta` que o front envia** (regra §8). O front já aplica o fator e envia o delta correto (ver §5). Logo o backend de lote **não precisa reconsultar a Sonda** — o cap defensivo em `:441-443` (`novaQtd > qtdEsp → novaQtd = qtdEsp`) permanece como **defesa** contra estados inconsistentes.
- **Contadores scan/manual:** `processarLoteDeltas` já calcula `deltaEfetivo = novaQtd - qtdAnterior` após o cap (`:445-457`) e distribui no contador certo conforme `metodo_entrada`. Com o front **bloqueando antes** de exceder (não enviando delta que estoura), o cap raramente dispara; quando disparar, o `deltaEfetivo` garante que `quantidade_bipada_scan/manual` não ultrapassam o total → **sem divergência de contadores** (resolve a dúvida da spec). Recomendação: incluir `fator`/`tipo_codigo_barras` no payload de `:475-484` também, por simetria de auditoria, lendo de `$bip['fator'] ?? null` (o front passa a enviar — ver §5). Baixo risco, opcional, marcado como "nice-to-have" no item de implementação.

---

## 4. Presenters — montar `codigosBarras = { codigo: fator }` por item (1 query/missão)

Os três pontos que montam `codigos_validos` precisam **adicionar** `codigosBarras` (mantendo `codigos_validos` por compatibilidade — regra de não-regressão).

### 4.1 `MissaoPresenter::montarJsonFrontendBase()` (`:80-105`) — ponto central, evita N+1

- **Antes do loop**, coletar `produto_id` de todos os itens e fazer **uma** chamada:
  `$mapaCodigos = (new ProdutoCodigoBarraSonda())->listarPorProdutos(array_map(fn($i) => $i->produto_id ?? 0, $itens));`
- **Dentro do loop**, para cada item, montar `codigosBarras`:
  - Começar pelos códigos do **produto** com fator 1: `codigo_barras`, `codigo_alternativo`, `referencia_id`/`codigo_produto`, `id` (mesma lista do `codigos_validos`), cada um → `1`.
  - **Depois** sobrescrever/adicionar com os da Sonda (`$mapaCodigos[$item->produto_id]`), cada `codigo_barras => fator`. Como vêm por último, **a tabela vence** se o código coincidir (precedência §1). Chaves normalizadas `trim()` (o front normaliza `toUpperCase()` no `buildCodeMap`).
  - Resultado: `$jsItensData[$item->id]['codigosBarras'] = [ "<cod>" => <fator>, ... ]`.
- `codigos_validos` **permanece** como está (`:97-101`).
- **Assinatura:** `montarJsonFrontendBase(array $itens)` não muda; a chamada à Sonda é interna. Como SeparacaoPresenter e PesagemPresenter chamam este método e **depois** sobrescrevem `codigos_validos` por item (`:58-63`), eles passam a **também** preencher/manter `codigosBarras` (ver 4.2).

### 4.2 `SeparacaoPresenter::present()` e `PesagemPresenter::present()` (`:58`, idênticos)

- Hoje recomputam `codigos_validos` por item no loop (`:58-63`). Passam a **também** setar `codigosBarras` no item, reusando o `$mapaCodigos`. Duas opções, escolhida a mais DRY:
  - **Decisão:** mover a montagem de `codigosBarras` para `montarJsonFrontendBase` (4.1) — como os Presenters específicos não apagam `codigosBarras` (só sobrescrevem `codigos_validos`/`produto_id`/etc.), o `codigosBarras` montado na base **sobrevive**. Os Presenters específicos **não precisam tocar `codigosBarras`**; só garantir que não o removem. Isso concentra a query e a lógica de fator num lugar só (menos duplicação entre Separação e Pesagem).
  - Caso o time prefira fator também refletindo o `produto_id`/`codigo_alternativo` que o Presenter específico adiciona (e a base não tem), incluir esses campos na lista de fator-1 dentro da base (eles já existem no item). **Recomendado:** incluir os 4 campos do produto na base, igual ao `codigos_validos` ampliado dos Presenters específicos, para o `codigosBarras` ser superset coerente.

### 4.3 `execucao_bipagem.php` (`:91-113`)

- Este template faz `array_merge($itemBase, [...])` onde `$itemBase` vem de `montarJsonFrontendBase`. Hoje preserva `codigos_validos` via `$itemBase['codigos_validos'] ?? ...` (`:106`). **Adicionar a mesma preservação para `codigosBarras`:** `'codigosBarras' => $itemBase['codigosBarras'] ?? []`. Sem isso, o merge não perde (não há chave conflitante), mas a explicitação evita regressão se alguém reordenar o array. **Baixo toque, 1 linha.**

> **N+1 evitado:** uma chamada `listarPorProdutos` por render de missão (não por item). Para uma missão de N itens, é **1** query à Sonda + as queries já existentes. Índice `idx_pcb_produto_block` cobre.

---

## 5. Frontend — `separacao/item.js` e `carregamento/item.js`

O contrato `item.codigosBarras = { codigo: fator }` é **confirmado** (já consumido por `buildCodeMap` nos dois arquivos). O trabalho é corrigir o incremento do caminho "item já selecionado" e alinhar validação/flags.

### 5.1 Separação — `componentes/Missoes/Logistica/sinais/App/html/separacao/item.js`

**(a) `buildCodeMap` (`:697-737`) — corrigir guarda do objeto vazio.**
Hoje entra no ramo `codigosBarras` se `typeof === 'object'` (`:705`), **mesmo se `{}`** → marca `_temCodigosEspecificos = true` com mapa vazio, podendo bloquear itens sem código. O carregamento já tem o `[FIX]` (`:890`: `Object.keys(...).length > 0`). **Alinhar a separação ao carregamento:** só tratar como específico se `Object.keys(item.codigosBarras).length > 0`; senão cair no ramo `codigos_validos`/flexível. **Também** popular `item._codigosSet` (Set das chaves normalizadas) na separação, como o carregamento faz (`:902`), para o lookup O(1) e para unificar a validação (ver (b)).

**(b) `processarScan`, caminho item-selecionado (`:2182-2217`) — usar fator do codeMap + validar por `_codigosSet`.**
Hoje (`:2189-2200`): valida via `itemAtual.codigos_validos` (array) e chama `ajustarQuantidade(1, ...)` fixo. Mudanças:
- **Validação:** trocar a checagem por `_codigosSet` (alinha com o `codigosBarras` real e com o carregamento). Se `_codigosSet` ausente/vazio mas há `codigos_validos`, lazy-init o Set (mesmo fallback do carregamento `:2797-2801`). Mantém o `|| !this.config.validarCodigo` para o modo flexível.
- **Quantidade:** obter a quantidade do mapa: `const q = (this.codeMap[cleanCode] && this.codeMap[cleanCode].quantidade) || 1;` e chamar `this.ajustarQuantidade(q, cleanCode);` (em vez de `1`). Assim, mesmo com item já selecionado, bipar caixa de fator N soma N (US2, AC2.1).
- O ramo "item sem códigos definidos → flexível" (`:2209-2215`) continua somando `1` (não há fator conhecido) — correto.

**(c) Caminho sem item selecionado (`:2219-2230`) — já correto.** Já faz `ajustarQuantidade(quantidade, ...)` com a quantidade do `codeMap`. Nenhuma mudança.

**(d) `ajustarQuantidade` (`:1971-2084`) — teto e extrato, sem mudança.**
- Teto (`:2002-2012`): já bloqueia `novaQty > limiteReal` para não-pesável e isenta KG. Com `delta = fator`, o bloqueio de caixa que excede já funciona (US4 no front). **Não mexer.**
- Extrato (`:2092-2101`): já envia `quantidade_delta: ultimoDelta` (= fator) e `metodo_entrada`. **Adicionar** `fator: ultimoDelta` e, se disponível, `tipo_codigo_barras` (pode-se derivar do item/codeMap; opcional). O backend de lote lê `quantidade_delta` — já coberto.

**(e) `config.validarCodigo` (`:54`) — já `true` na separação.** Nada a fazer (AC6.1). Mas reforçar o alinhamento de "item sem código + validação ligada → bloquear" (AC6.3): a separação hoje, no ramo sem códigos, é flexível (`:2209-2215`, `:2237-2243`). **Decisão:** alinhar à semântica do carregamento (`:2820-2839`) — com `validarCodigo` ligado e item sem nenhum código cadastrado, **bloquear** com mensagem "produto sem código cadastrado" (em vez de aceitar flexível). Isso satisfaz AC6.3 e torna separação e carregamento previsíveis. (Itens **com** código continuam validando normalmente; só muda o caso degenerado "zero códigos".)

> Observação de comportamento: como os Presenters passam a **sempre** popular `codigos_validos` (4 campos do produto) + `codigosBarras`, na prática um item só fica "sem código" se o produto não tiver nenhum dos 4 campos nem registro na Sonda — caso raro e legítimo de bloqueio.

### 5.2 Carregamento — `componentes/Missoes/Logistica/sinais/App/html/carregamento/item.js`

**(a) `config.validarCodigo` (`:54`) — mudar default `false → true`** (AC6.2). E a leitura de config (`:462-470`): hoje, se `#data-validar-codigo-conf` está `undefined`, cai em `false` (`:466-467`). **Decisão (resolve a dúvida da spec):** mudar o fallback de `undefined` para `true` (validação ligada por padrão), espelhando a separação. Não é preciso novo `data-attribute` nem config de backend: quando a view fornece `#data-validar-codigo-conf`, ele continua mandando (`'1'`/`'0'`); só o **default na ausência** vira ligado. Conferir que a view de carregamento não está fixando `'0'` em algum ponto — se estiver, remover/ajustar para herdar o default ligado. (Item de verificação na implementação; a flag de config por missão, se existir no backend, deve refletir "ligado" como novo default.)

**(b) `buildCodeMap` (`:881-925`) — já correto.** Já tem o guard de objeto vazio (`:890`) e popula `_codigosSet` (`:902,917`). Nenhuma mudança.

**(c) `processarScan`, caminho item-selecionado (`:2790-2841`) — usar fator do codeMap.**
Hoje (`:2804-2810`): valida via `_codigosSet` (correto) mas chama `ajustarQuantidade(1, ...)` fixo. **Única mudança:** trocar o `1` pela quantidade do mapa: `const q = (this.codeMap[cleanCode] && this.codeMap[cleanCode].quantidade) || 1; this.ajustarQuantidade(q, cleanCode);`. O resto (bloqueio de item sem código com validação ligada `:2820-2832`) já está correto (AC6.3).

**(d) Caminho sem item selecionado (`:2843-2853`) — já correto.** `ajustarQuantidade(quantidade, ...)`. Nada a fazer.

**(e) `ajustarQuantidade` (teto) e extrato (`:2716-2724`, `:4420-4434`) — sem mudança estrutural.**
- Teto: análogo à separação, já bloqueia não-pesável e isenta peso. Com `delta = fator`, US4 coberto no front.
- `montarBipagem`/extrato (`:4420-4434`): já envia `quantidade_delta: delta` (= fator) e `metodo_entrada`. **Adicionar** `fator: delta` (e `tipo_codigo_barras` se disponível) por auditoria. `quantidade_delta` em `:2717` e `:4423` continua sendo a fonte que o backend de lote consome.

### 5.3 Resumo das mudanças de front (precisas)
| Arquivo | Local | Mudança |
|---|---|---|
| separacao/item.js | `buildCodeMap` `:705` | guard `Object.keys().length>0` + popular `_codigosSet` |
| separacao/item.js | `processarScan` `:2189-2200` | validar por `_codigosSet`; `ajustarQuantidade(qDoMapa,...)` |
| separacao/item.js | `processarScan` ramo "sem código" `:2209-2215`/`:2237-2243` | com `validarCodigo` ligado, bloquear (AC6.3) |
| separacao/item.js | extrato `:2092` | add `fator` (e `tipo_codigo_barras`) — opcional |
| carregamento/item.js | `config` `:54` + init `:466-467` | default `validarCodigo = true` |
| carregamento/item.js | `processarScan` `:2810` | `ajustarQuantidade(qDoMapa,...)` |
| carregamento/item.js | extrato `:4423`/`:2717` | add `fator` — opcional |

---

## Padrões de extensibilidade

**N/A.** Não há Adapter, Registry ou Interface nova. Justificativa explícita (anti-padrão prematuro): a spec descreve **um** modelo de resolução de código (campos do produto + tabela `produtos_codigos_barras`) e **um** consumidor do fator (a bipagem de separação/carregamento). Introduzir `Strategy`/`Registry` aqui não tem requisito de variabilidade que o justifique — diferente de `Edicaorota/Callbacks/Registry`, que existe porque múltiplos módulos reagem ao mesmo evento. A extensibilidade real (novos tipos de código de barras) já é dados (`tipo_codigo_barras`), não código.

---

## Ordem de implementação (sequencial)

1. **Migration** — `001_create_produtos_codigos_barras.sql`. Validar idempotência rodando contra `dev_gx_goldie` (a tabela já existe → deve ser no-op sem erro). [read-only do Tech Lead já confirmou schema; aplicar é do dev.]
2. **Sonda** — `resolverPorCodigo()` + `listarPorProdutos()` em `ProdutoCodigoBarraSonda`. `php -l` + teste de integração da Sonda (passo 7).
3. **Backend `MissoesBipagem`** — `resolverProduto` (consulta Sonda, anexa fator + propriedade de request), `biparItem` (fator como delta), payload do extrato (`fator`/`tipo_codigo_barras`). Confirmar bloqueio por teto (sem alterar `validarLimites`).
4. **Presenters** — `montarJsonFrontendBase` (1× `listarPorProdutos`, montar `codigosBarras` com precedência da tabela) + garantir preservação em Separação/Pesagem e em `execucao_bipagem.php`.
5. **Frontend separação** — `buildCodeMap` guard + `_codigosSet`; `processarScan` item-selecionado (fator + validação por Set); bloqueio "sem código" com validação ligada.
6. **Frontend carregamento** — default `validarCodigo = true`; `processarScan` item-selecionado (fator do mapa).
7. **Testes** — TDD/integração (próxima seção).
8. **i18n** — strings nas 3 línguas (próxima seção).

> Backend (1-4) antes do front (5-6): o front depende do `codigosBarras` vindo dos Presenters e o lote depende do contrato de delta. Testes (7) acompanham cada camada (TDD), com a suíte de integração ao final.

---

## Plano de teste

- **Unit/TDD (sem banco) — Sonda e resolução:**
  - `resolverPorCodigo`: monta critério correto com/sem `produtoId`; normaliza e rejeita vazio; retorna `null` quando não acha. (Pode ser exercitado via integração — ver abaixo — por depender de `find()`.)
  - `MissoesBipagem`: precedência (Sonda > campo do produto); fator vira delta em `biparItem`; manual continua `±1`; payload do extrato contém `fator`/`tipo_codigo_barras`. Exercitar com fixtures de `MissaoLogisticaItem` + `produtos_codigos_barras` na suíte de integração.
- **Integração contra `dev_gx_goldie`** — novo `componentes/Constelacoes/Logistica/MissaoLogistica/Testes/Laboratorios/FatorBipagemIntegrationTest.php`, espelhando o bootstrap de `CodigosBarrasIntegrationTest.php` (HTTP_HOST `dev-...`, `$_SESSION['galaxiaUserInfo']` na raiz **e** em `$_SESSION['dashboard']`, roteamento gx_goldie→dev_gx_goldie, cleanup em `finally`). Casos:
  1. Seed: 1 produto sentinela + 1 registro em `produtos_codigos_barras` com `fator=10` (via a própria `ProdutoCodigoBarraSonda`/sincronizador, no `block` sentinela `999777`). `resolverPorCodigo` retorna o registro com `fator=10`; código de outro tenant **não** resolve (AC1.5).
  2. `listarPorProdutos([pid])` agrupa por `produto_id` e devolve `fator` float.
  3. Resolução estrita: código da Sonda + `produtoId` correto → fator 10; código do campo do produto → fator 1; precedência: código presente nos dois → fator da tabela.
  4. Fluxo `biparItem` (com missão+item sentinela): bipar caixa fator 10 num item que pede ≥10 → `quantidade_bipada += 10`, **1** extrato com `delta=10` e `fator=10` no payload (AC3.1/AC3.2).
  5. Bloqueio: bipar caixa fator 10 num item que pede 7 → erro, `quantidade_bipada` inalterada (AC4.1); completar com 7 unitárias funciona (AC4.4).
  6. Pesável (KG): fator não bloqueia / comportamento de peso preservado (AC4.3) — validar `validarLimites` retorna null para KG.
  7. Não-regressão: produto sem registro na Sonda → bipar código do produto soma 1, 1 extrato (AC2.2).
  - **Rodar:** `php componentes/Constelacoes/Logistica/MissaoLogistica/Testes/Laboratorios/FatorBipagemIntegrationTest.php`
- **Manual (smoke do solicitante):**
  - Separação: bipar 1× uma caixa fator 10 num item que pede ≥10 → mostra "10/...", 1 linha no extrato. Bipar caixa fator 10 num item que precisa de 7 → bloqueado; completar com 7 unitárias funciona.
  - Carregamento: bipar código que **não** pertence ao item → recusado (validação agora ligada); bipar código da Sonda do item → aceito com fator.
  - Produto sem códigos adicionais → fluxo atual intacto.

---

## Riscos e decisões não-óbvias

1. **Propagação do fator via objeto (decisão tomada).** O fator da leitura é anexado ao produto em `resolverProduto`, propagado ao item em `buscarItemDaMissao` e lido em `biparItem`; o tipo segue pela assinatura estendida de `alterarQuantidade`/`atualizarItemNoBanco`. **Não** se usa propriedade mutável do Satélite (evita acoplamento temporal). Custo: 2 assinaturas ganham 1 parâmetro opcional no fim (compatível com chamadas legado). Risco residual: baixo — o fator viaja com o objeto da própria chamada.
2. **Precedência do fator depende da ORDEM de montagem** (Sonda por último no front; Sonda antes no back). Risco: regressão silenciosa se alguém reordenar. Mitigação: comentar a intenção nos dois pontos e cobrir com o caso de teste 3 (código nos dois lugares → fator da tabela).
3. **`buildCodeMap` da separação tratava `{}` como "tem códigos específicos"** (`:705`), o que poderia bloquear itens com `codigosBarras` vazio. Como agora os Presenters **sempre** mandam pelo menos os códigos do produto (fator 1), o objeto raramente é `{}`; ainda assim aplicamos o guard `length>0` (paridade com carregamento) para blindar o caso degenerado. Risco residual: baixo.
4. **Carregamento ligando `validarCodigo` por default** pode endurecer operações que hoje aceitam qualquer beep. Risco operacional (não técnico): itens sem código cadastrado passam a bloquear. Mitigação: decisão TRAVADA pela spec (US6/AC6.2-6.3); a flag por missão (`#data-validar-codigo-conf`) ainda permite override explícito por configuração se a view a fornecer. Verificar na implementação se a view de carregamento fixa `'0'`.
5. **`fator` é `NUMERIC(6,2)`** → **confirmado lendo o `dev_gx_goldie`**: o driver PDO pgsql devolve **string com ponto** (`"5.00"`), e `floatval("5.00") = 5.0` ✓. A vírgula (`5,00`) que aparece em clientes de banco é só formatação de locale, não chega ao PHP. Mitigação aplicada: `floatval()` em todos os consumos no PHP (`resolverPorCodigo`/`biparItem`); `parseFloat` já no `buildCodeMap` do front (`:707`). Sem tratamento especial de vírgula necessário.
6. **Fração de fator** (ex.: `2.50`) é tecnicamente possível pelo schema. A spec trata fator como "unidades por caixa" (inteiro na prática). O teto da bipagem já tem tolerância decimal (`ceil` em `validarLimites:240` e no front `:1995-1997`). Não tratamos fator fracionário como caso de produto explícito; comportamento herda o teto atual. Sem ação, anotado.
7. **`codigo_alternativo` não existe na tabela `produtos` do `dev_gx_goldie`** (confirmado: só `codigo_barras`, `codigo_produto`, `id`, `tipo_venda`). O código atual já referencia `codigo_alternativo` via `?? ''` e `findAny` (tolerado). **Não está no escopo** corrigir isso; manter o comportamento atual (campo ausente → vazio/ignorado). Anotado para não surpreender o dev.

---

## Breaking changes

**Nenhum breaking de contrato.** Apenas adições e correções de comportamento previstas pela spec:
- `codigos_validos` mantido (compat). `codigosBarras` é aditivo.
- `resolverProduto` mantém o retorno (objeto produto) — fator anexado como propriedade dinâmica (não quebra consumidores existentes).
- Payload do extrato ganha chaves (`fator`, `tipo_codigo_barras`) — append-only, consumidores ignoram chaves desconhecidas.
- **Mudança de comportamento intencional (não-breaking de API):** carregamento passa a validar código por padrão (US6) e a separação passa a bloquear o caso "item sem nenhum código" com validação ligada (AC6.3). Documentado como esperado.

---

## i18n

Strings novas/alteradas (PT-BR / EN / ES) em `linguagens/{pt-br,en,es}.json`. As mensagens hoje estão **hardcoded** no PHP (`MissoesBipagem`) e no JS; a feature padroniza as relevantes ao bloqueio/erro. Chaves sugeridas (manter convenção do projeto de chave = texto-fonte ou slug, conforme `genesis-views-i18n` — alinhar com o time no detalhe):

| Chave (sugerida) | PT-BR | EN | ES | Onde |
|---|---|---|---|---|
| `bipagem_codigo_nao_pertence_item` | "Código não pertence a este item!" | "Code does not belong to this item!" | "¡El código no pertenece a este ítem!" | front (separação `:2207`, carregamento `:2817`) |
| `bipagem_produto_sem_codigo` | "Produto sem código cadastrado!" | "Product has no registered code!" | "¡Producto sin código registrado!" | front (carregamento `:2827`; novo na separação AC6.3) |
| `bipagem_volume_excedido` | "Quantidade excedida! Limite: %s" | "Quantity exceeded! Limit: %s" | "¡Cantidad excedida! Límite: %s" | back `validarLimites:243` + `alertaVolumeMaximo` no front |
| `bipagem_codigo_invalido_produto` | "Código inválido: produto não cadastrado (%s)" | "Invalid code: product not registered (%s)" | "Código inválido: producto no registrado (%s)" | back `resolverProduto:139` |
| `bipagem_codigo_incorreto_divergente` | "Código incorreto! (%s) não pertence a %s" | "Wrong code! (%s) does not belong to %s" | "¡Código incorrecto! (%s) no pertenece a %s" | back `resolverProduto:128` |

> Mensagens de bloqueio por **fator que excede** reusam `bipagem_volume_excedido` (é o mesmo teto). Não há string nova exclusiva de "caixa excede" — a spec pede coerência com o bloqueio de teto atual (AC4.2). Detalhe de mecânica i18n (função `show()`/dicionário) fica com a skill `genesis-views-i18n` na implementação; aqui listamos o inventário.

---

## Resolução das "Dúvidas em aberto" (do §final da spec)

1. **Local da migration** → `componentes/Constelacoes/Produtos/CodigosBarra/Migrations/001_create_produtos_codigos_barras.sql`, PostgreSQL, idempotente. Índices = os reais (`idx_pcb_produto_block`, `idx_pcb_codigo_block`, `idx_pcb_codigomd5`, `idx_pcb_tipo`) — já cobrem as leituras; nenhum índice novo.
2. **Contrato do front** → `item.codigosBarras = { codigo: fator }` confirmado. Separação migra a validação de `codigos_validos` para `_codigosSet` (paridade com carregamento `:902`), e o `buildCodeMap` da separação ganha o guard de objeto vazio.
3. **Origem do fator na resolução estrita** → ordem nos dois caminhos de `resolverProduto`: **Sonda primeiro** (fator da tabela, por código), **campos do produto depois** (fator 1). O fator é anexado ao objeto produto, propagado ao item em `buscarItemDaMissao` e lido por `biparItem`; o tipo segue pela assinatura estendida de `alterarQuantidade`/`atualizarItemNoBanco` (sem propriedade mutável).
4. **`bipagemLote`/snapshot** → cap em `:441-443` permanece como defesa; front bloqueia antes (não envia delta que excede). `deltaEfetivo` (`:447`) garante contadores scan/manual consistentes mesmo se o cap disparar → sem divergência. Sem mudança de lógica no lote.
5. **Default de `validarCodigo` no carregamento** → mudar `config.validarCodigo` (`:54`) para `true` e o fallback de `undefined` (`:466-467`) para `true`. Sem novo `data-attribute`; `#data-validar-codigo-conf` continua mandando quando presente.
6. **Chaves do payload do extrato** → `fator` (float) e `tipo_codigo_barras` (string|null), adicionadas ao payload existente em `atualizarItemNoBanco:285-296` (e por simetria no lote `:475-484`).
7. **i18n** → ver tabela acima; 5 chaves, 3 línguas; bloqueio por fator reusa `bipagem_volume_excedido`.

---

## Out of scope (confirmando a spec + acréscimos técnicos)

Da spec (mantidos intactos):
- Inteligência V1 `ProdutoCodigoBarra` (singular), `CodigosBarra/sql/produto_codigo_barra.sql`, Satélite `ProdutosCodigosBarra` — **código morto, não tocar**.
- Toggle para desligar validação automática; override de divergência; fluxo dedicado de digitação manual; aplicar fator no +/-; CRUD/sincronização do ERP; conferência (`conferencia*`) e inventário.

Acréscimos técnicos (fora deste plano):
- Corrigir a referência a `produtos.codigo_alternativo` ausente no schema (risco 7) — comportamento atual preservado, não corrigido aqui.
- Internacionalizar **todas** as mensagens hardcoded de `MissoesBipagem`/`item.js` — só as do inventário i18n acima entram; varredura completa fica para outra task.
- Tratamento de fator fracionário como caso de produto explícito (risco 6) — herda o teto atual.
- Nenhuma rota `/api/v1` é criada ou alterada.
