# Sugestão de Código de Barras na Separação e Carregamento

**Data:** 2026-05-26
**Escopo:** Sinal novo `SugestoesCodigoBarras` (vertical slice V2) em `componentes/Missoes/Logistica/sinais/` + 1 hook no core (`MissoesStatus`) + partial compartilhada em `App/html/shared/` consumida por `separacao/item.php` e `carregamento/item.php`.
**Banco:** todo trabalho contra `dev_gx_goldie`.
**i18n:** somente `pt-br.json` nesta rodada.

---

## 1. Problema

Durante a separação/carregamento, o operador frequentemente encontra produtos **sem `codigo_barras`** cadastrado. Hoje a bipagem aceita qualquer código nesses casos sem validação (`carregamento_bipagem.js:260`), perdendo a oportunidade de **capturar o código real** que o operador está vendo na embalagem.

Queremos permitir ao operador **sugerir** o código de barras correto durante a execução, e transformar essas sugestões num **fluxo de aprovação pelo gestor** ao final da missão — alimentando o sistema existente de Ajuste de Dados (que já sincroniza com o ERP via `update_dados_logistica`).

## 2. Solução em uma linha

Quando o operador finaliza uma separação/carregamento que recebeu sugestões pendentes, o sistema **cria automaticamente uma missão `AJUSTE_DADOS`** já em status `AGUARDANDO_APROVACAO`, com 1 item por sugestão, descrição `"Sugestão - Separação #<id>"` (ou Carregamento). Se nenhuma sugestão foi enviada, nenhuma missão é criada.

## 3. Decisões-chave (resumo das perguntas)

| # | Decisão | Valor |
|---|---|---|
| 1 | UX trigger | Botão "Sugerir código" sempre visível em produtos sem `codigo_barras` |
| 2 | Agrupamento | 1 missão AJUSTE_DADOS por missão de origem |
| 3 | Persistência | Incremental — cada sugestão vai pro servidor na hora |
| 4 | Líder/operador da AJUSTE | Sem líder; quem sugeriu fica em `dados_adicionais` |
| 5 | Escopo de campos | Somente `codigo_barras` |
| 6 | Título | `"Sugestão - Separação #<id>"` / `"Sugestão - Carregamento #<id>"` |
| 7 | Edit/cancel | Operador pode editar e cancelar até finalizar |
| 8 | Validações | tamanho ≥ 6; não pode duplicar `produtos.codigo_barras`; não pode duplicar outra sugestão pendente |

## 4. Fluxo end-to-end

```
Operador abre item → produto SEM codigo_barras → vê botão "Sugerir código"
   ↓ clica
Modal: input + Confirmar/Cancelar
   ↓ confirma
POST sugerir → valida (tamanho, unicidade produtos, unicidade sugestões pendentes)
   → upsert em missao_codigo_sugestao (status=pendente)
   → 200 OK
Botão vira chip "Sugerido: <codigo> [✎] [✕]"
   ↓ pode editar (mesmo POST = upsert) ou cancelar (status=cancelada, soft-delete)

Operador finaliza a missão (botão existente "Concluir")
   → MissoesStatus::concluirOuFinalizarOperador transiciona pra CONCLUIDA/AGUARDANDO_APROVACAO
   → APÓS transição com sucesso, hook chama GerarAjusteDeSugestoesLaboratorio
   → Lê sugestões pendentes da origem
     • 0 sugestões → return (nenhuma missão criada)
     • ≥1 sugestões → cria AJUSTE_DADOS via Adapter, status AGUARDANDO_APROVACAO
   → Marca sugestões como `consumida` + grava consumida_em_ajuste_id

Gestor abre painel de AJUSTE_DADOS:
   → vê "Sugestão - Separação #1532"
   → aprova / rejeita pelo fluxo existente (zero alteração)
   → ao aprovar, AjusteDadosHookStrategy dispara update_dados_logistica pro ERP
   → produto ganha codigo_barras no Genesis + ERP
```

**Princípios:**
- Aprovação e webhook do ERP ficam **inalterados** — reaproveita 100%.
- A missão AJUSTE nasce direto em `AGUARDANDO_APROVACAO` (pula execução: já aconteceu na origem).
- Sugestão é **idempotente** por `(missao_origem_id, produto_id)`.
- Sem líder/operador na missão AJUSTE; rastreabilidade fica em `dados_adicionais.operador_sugeriu_id` por item.

## 5. Estrutura do Sinal (vertical slice V2)

```
componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/
├── SugestoesCodigoBarras.php              # controller dispatcher fino
├── Migrations/
│   └── 001_create_missao_codigo_sugestao.sql
├── Sondas/
│   └── MissaoCodigoSugestaoSonda.php       # ModelInteligencia V2
├── Enums/
│   └── StatusSugestao.php                  # PENDENTE | CONSUMIDA | CANCELADA
├── Validators/
│   ├── SugerirCodigoBarrasValidator.php
│   └── CancelarSugestaoValidator.php
├── Laboratorios/
│   ├── SugerirCodigoBarrasLaboratorio.php
│   ├── CancelarSugestaoLaboratorio.php
│   ├── ListarSugestoesLaboratorio.php
│   └── GerarAjusteDeSugestoesLaboratorio.php
├── Adapters/
│   └── AjusteDados/
│       ├── Laboratorios/
│       │   └── CriarAjusteDadosDeSugestoesLaboratorio.php
│       └── Testes/Laboratorios/*TddTest.php
└── Testes/
    ├── Sondas/MissaoCodigoSugestaoSondaTddTest.php
    ├── Validators/SugerirCodigoBarrasValidatorTddTest.php
    ├── Laboratorios/SugerirCodigoBarrasLaboratorioTddTest.php
    ├── Laboratorios/CancelarSugestaoLaboratorioTddTest.php
    ├── Laboratorios/GerarAjusteDeSugestoesLaboratorioTddTest.php
    └── Enums/StatusSugestaoTddTest.php
```

**Frontend** (mexe em código legado):

```
componentes/Missoes/Logistica/sinais/App/html/shared/sugestao_codigo_barras.php   (NOVO)
componentes/Missoes/Logistica/sinais/App/html/shared/sugestao_codigo_barras.js    (NOVO)
componentes/Missoes/Logistica/sinais/App/html/separacao/item.php                  (include)
componentes/Missoes/Logistica/sinais/App/html/carregamento/item.php               (include)
linguagens/pt-br.json
```

## 6. Schema do banco

Migration `Migrations/001_create_missao_codigo_sugestao.sql`:

```sql
CREATE TABLE IF NOT EXISTS missao_codigo_sugestao (
    id                       SERIAL PRIMARY KEY,
    missao_origem_id         INTEGER NOT NULL
                             REFERENCES missao_logistica(id) ON DELETE CASCADE,
    produto_id               INTEGER NOT NULL,
    codigo_sugerido          VARCHAR(64) NOT NULL,
    operador_sugeriu_id      INTEGER NOT NULL,
    status                   VARCHAR(20) NOT NULL DEFAULT 'pendente',
                             -- 'pendente' | 'consumida' | 'cancelada'
    consumida_em_ajuste_id   INTEGER NULL
                             REFERENCES missao_logistica(id) ON DELETE SET NULL,
    consumida_em             TIMESTAMP NULL,
    cancelada_em             TIMESTAMP NULL,

    -- Block padrão Galaxia (preenchido pelo framework via customerControl/companyControl;
    -- sem DEFAULT — não declarar e não preencher manualmente)
    data_cadastro            TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
    block_filial_id          INTEGER,
    block_usuario_id         INTEGER,
    usuario_cadastro         VARCHAR(255),
    data_atualizacao         TIMESTAMP WITHOUT TIME ZONE,
    usuario_atualizacao      VARCHAR(255),
    block_data               TIMESTAMP WITHOUT TIME ZONE,
    block_quem               VARCHAR(255),
    block                    INTEGER,
    block_empresa_id         INTEGER
);

CREATE UNIQUE INDEX uniq_missao_codigo_sugestao_pendente
    ON missao_codigo_sugestao (missao_origem_id, produto_id, block)
    WHERE status = 'pendente';

CREATE INDEX idx_missao_codigo_sugestao_codigo_status
    ON missao_codigo_sugestao (codigo_sugerido, status, block);

CREATE INDEX idx_missao_codigo_sugestao_origem
    ON missao_codigo_sugestao (missao_origem_id, status, block);
```

**Decisões:**
- `status='cancelada'` é **soft-delete** — preservamos auditoria.
- Unique index **parcial** (`WHERE status='pendente'`) garante idempotência somente entre pendentes. Sugestão cancelada pode coexistir com nova pendente do mesmo `(origem, produto)`.
- `consumida_em_ajuste_id` cria rastreio reverso ("essa sugestão virou qual ajuste").
- Colunas `block_*` sem `DEFAULT` — framework injeta via `customerControl`/`companyControl`.

## 7. Sonda

`Sondas/MissaoCodigoSugestaoSonda.php` (extends `ModelInteligencia`):

```php
protected $entity = 'missao_codigo_sugestao';
protected $required = ['missao_origem_id', 'produto_id', 'codigo_sugerido', 'operador_sugeriu_id'];
```

Métodos finos (sem regra de negócio):
- `upsertPendente(int $origemId, int $produtoId, string $codigo, int $operadorId): object`
- `cancelarPendente(int $origemId, int $produtoId): bool`
- `listarPendentesPorOrigem(int $origemId): array`
- `existeOutraPendenteComMesmoCodigo(string $codigo, int $produtoIdExcluir, int $block): bool`
- `marcarComoConsumidas(array $sugestaoIds, int $ajusteId): void`

## 8. Validators

**`SugerirCodigoBarrasValidator::validar($data, $blockAtual)`** valida em ordem:

1. Campos obrigatórios (`missao_origem_id`, `produto_id`, `codigo_sugerido` não vazio).
2. `strlen($codigo) >= 6`.
3. Missão de origem existe, é SEPARACAO ou CARREGAMENTO, **não está** em status finalizado (`CONCLUIDA`, `AGUARDANDO_APROVACAO`, `CANCELADA`).
4. Produto existe e **não tem `codigo_barras`** (regra do fluxo).
5. Código não coincide com `produtos.codigo_barras` de outro produto.
6. Código não está em outra sugestão pendente (de outro produto).

Erros usam chaves i18n de `sugestao.erro.*`.

**`CancelarSugestaoValidator`**: valida que existe pendente pra `(missao_origem_id, produto_id)` e missão ainda não finalizada.

## 9. Endpoints (via dispatcher do Sinal)

Não criamos handlers REST `/api/v1`. Padrão clássico Genesis:

| `sinalGxData` | Roteia pra | Retorno |
|---|---|---|
| `sugerir` | `SugerirCodigoBarrasLaboratorio` | `{ ok: true, sugestao: {...} }` ou `{ ok: false, erro: '...' }` |
| `cancelar` | `CancelarSugestaoLaboratorio` | `{ ok: true }` ou `{ ok: false, erro: '...' }` |
| `listar` | `ListarSugestoesLaboratorio` | `{ ok: true, sugestoes: [...] }` |

Controller `SugestoesCodigoBarras.php` (dispatcher fino, padrão Edicaorota):

```php
public function start(?array $data = [])
{
    $acao = $data['sinalGxData'] ?? '';
    return match ($acao) {
        'sugerir'  => (new SugerirCodigoBarrasLaboratorio())->executar($data),
        'cancelar' => (new CancelarSugestaoLaboratorio())->executar($data),
        'listar'   => (new ListarSugestoesLaboratorio())->executar($data),
        default    => (new Message())->error('Ação inválida')->render(),
    };
}
```

## 10. Laboratórios

**`SugerirCodigoBarrasLaboratorio::executar($data)`**
1. Pega `block` e `operadorId` da sessão (`galaxiaUserInfo`).
2. Valida via `SugerirCodigoBarrasValidator`.
3. Upsert na Sonda: se já existe pendente pra `(origem, produto)`, atualiza `codigo_sugerido` + `operador_sugeriu_id` + `block_user_update`; senão insere.
4. Retorna `['ok' => true, 'sugestao' => $sugestao]`.

**`CancelarSugestaoLaboratorio::executar($data)`**
1. Valida.
2. Marca status=`cancelada`, `cancelada_em = NOW()`.
3. Retorna `['ok' => true]`.

**`ListarSugestoesLaboratorio::executar($data)`**
1. Lê pendentes da `missao_origem_id`.
2. Retorna `['ok' => true, 'sugestoes' => [...]]` — usado pelo front pra hidratar.

**`GerarAjusteDeSugestoesLaboratorio::executar($missaoOrigemId)`** (coração do fluxo)
1. Lê sugestões pendentes da origem.
2. Se 0 → return sem criar nada.
3. Lê missão origem pra montar título: `"Sugestão - {Separação|Carregamento} #{id}"`.
4. **Guard de idempotência**: se já existe AJUSTE_DADOS com `referencia_id = $missaoOrigemId` em status `AGUARDANDO_APROVACAO` → return (evita duplicação em caso de reentrancy).
5. Delega criação pro `CriarAjusteDadosDeSugestoesLaboratorio` (Adapter).
6. Chama `marcarComoConsumidas($ids, $ajusteId)` na Sonda.
7. Registra extrato na origem: `GEROU_AJUSTE_DE_SUGESTOES` com `{ajuste_id, total}`.

## 11. Adapter AjusteDados

**`Adapters/AjusteDados/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorio::executar($payload)`**

Por que Adapter: integração com o domínio AJUSTE_DADOS (criar missão, popular itens com snapshot) é específica e isolada. Se um dia trocar o jeito de criar ajustes, troca-se só o adapter.

Cria a missão via `MissoesPersistencia::criar()`:

```php
[
    'tipo_missao_id'   => TipoMissao::AJUSTE_DADOS->id(),
    'descricao'        => $titulo,
    'status'           => StatusMissao::AGUARDANDO_APROVACAO->value,
    'referencia_tipo'  => $tipoOrigemValue,    // 'SEPARACAO' ou 'CARREGAMENTO'
    'referencia_id'    => $missaoOrigemId,
    'lider_id'         => null,
    'operador_id'      => null,
    'requer_aprovacao' => 1,
    'finalizado_em'    => date('Y-m-d H:i:s'),
    'finalizada_por'   => null,
    'block'            => $origemBlock,
    'campos_ajuste'    => json_encode(['codigo_barras']),
]
```

Cria 1 item por sugestão via `MissoesPersistencia::batchInsertItens()`:

```php
// Produtos é acessado via galaxiaRoute (padrão Crud), não Sonda V2 (não existe ainda).
//   $produto = $galaxiaRoute
//       ->setConstelacao('Produtos')->setEstrela('Crud')->setSatelite('Produtos')
//       ->setOpcao('read')->dadosG(['id' => $s->produto_id])['produtosData'][0] ?? null;
foreach ($sugestoes as $s) {
    $produto = $this->lerProduto($s->produto_id);   // helper interno do Adapter
    $itens[] = [
        'missao_logistica_id' => $ajusteId,
        'produto_id'          => $s->produto_id,
        'status'              => 'pendente',
        'qtd_total'           => 1,
        'qtd_bipada'          => 1,
        'dados_adicionais'    => json_encode([
            'codigo_barras'          => $s->codigo_sugerido,   // nome alinhado com leitura atual do aprovador
            'codigo_barras_sugerido' => $s->codigo_sugerido,   // redundante mas explícito
            'codigo_barras_atual'    => $produto->codigo_barras ?? null,
            'operador_sugeriu_id'    => $s->operador_sugeriu_id,
            'origem_missao_id'       => $missaoOrigemId,
            'origem_sugestao_id'     => $s->id,
            'importacao_id'          => $produto->importacao_id ?? null,
        ]),
    ];
}
```

> **Pré-implementação**: verificar exatamente como `MissoesAprovacao::sincronizarErpAjusteDados` lê `codigo_barras` do `dados_adicionais` da designação manual; replicar o nome de campo pra zero alteração no aprovador.

Retorna `$ajusteId`.

## 12. Hook do core

Patch único em `componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesStatus.php::concluirOuFinalizarOperador`, **depois** de `$result = $this->transicionar(...)` ter sucesso:

```php
$tipoMissaoId = (int)($missao->tipo_missao_id ?? 0);
if (in_array($tipoMissaoId, [TipoMissao::SEPARACAO->id(), TipoMissao::CARREGAMENTO->id()], true)) {
    $cls = '\GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios\GerarAjusteDeSugestoesLaboratorio';
    if (class_exists($cls)) {
        try {
            (new $cls())->executar($missaoId);
        } catch (\Throwable $e) {
            error_log("[SugestoesCodigoBarras] falha ao gerar ajuste em missao={$missaoId}: " . $e->getMessage());
        }
    }
}
```

**Por que não falhar a finalização se a geração da AJUSTE der ruim:** finalização da separação é a operação crítica do operador. Falha no callback é loggada mas não bloqueia. Sugestões ficam `pendente` e podem ser re-processadas manualmente.

**Alternativa considerada (não adotada agora):** event bus em `Constelacoes/.../Eventos/MissaoEventosRegistry`. Adiciona infra (auto-discovery de bootstrap) que só vale pra um listener — refatoramos quando aparecer o segundo caso.

## 13. Aprovação pelo gestor (sem mudanças)

A missão AJUSTE_DADOS gerada flui pelo `MissoesAprovacao::aprovar` existente. Ao aprovar:
- Loop em `sincronizarErpAjusteDados` lê itens, monta `produtoUpdates` com `codigo_barras` do `dados_adicionais`, dispara `AjusteDadosHookStrategy::sincronizar()`.
- Webhook `update_dados_logistica` pro ERP atualiza `produtos.codigo_barras` (com `block` se PRD `hook-ajuste-dados-block` já mergeado).

## 14. Frontend

### Partial `shared/sugestao_codigo_barras.php`

Renderiza dentro do card do item. Só aparece se `empty($item->codigo_barras)`.

```php
<?php
if (!empty($item->codigo_barras)) return;
$missaoOrigemId = (int)$missaoId;
$produtoId      = (int)$item->produto_id;
?>
<div class="sugestao-codigo-barras"
     data-missao-origem="<?= $missaoOrigemId ?>"
     data-produto-id="<?= $produtoId ?>">

    <button type="button" class="btn btn-sm btn-outline-warning js-sugerir-codigo">
        <i class="bi bi-upc-scan"></i> <?= show('sugestao.botao_sugerir') ?>
    </button>

    <div class="chip-sugerido" hidden>
        <i class="bi bi-check2-circle"></i>
        <?= show('sugestao.label_sugerido') ?>: <span class="codigo"></span>
        <button type="button" class="btn-link js-editar-sugestao" title="<?= show('sugestao.acao_editar') ?>">
            <i class="bi bi-pencil"></i>
        </button>
        <button type="button" class="btn-link js-cancelar-sugestao" title="<?= show('sugestao.acao_cancelar') ?>">
            <i class="bi bi-x-lg"></i>
        </button>
    </div>
</div>
```

Modal (renderizada uma vez no fim da partial, fora do loop):

```html
<div id="modal-sugerir-codigo" class="modal" tabindex="-1" hidden>
    <div class="modal-content">
        <h5><?= show('sugestao.modal_titulo') ?></h5>
        <p class="produto-nome"></p>
        <input type="text" id="input-codigo-sugerido"
               class="form-control"
               minlength="6" maxlength="64"
               placeholder="<?= show('sugestao.modal_placeholder') ?>"
               autocomplete="off" inputmode="numeric">
        <div class="erro" hidden></div>
        <div class="actions">
            <button class="btn btn-secondary" data-action="fechar"><?= show('comum.cancelar') ?></button>
            <button class="btn btn-primary" data-action="confirmar"><?= show('comum.confirmar') ?></button>
        </div>
    </div>
</div>
```

### Bundle JS `shared/sugestao_codigo_barras.js`

IIFE com cache `sugestoesByProduto`, modal compartilhada, AJAX direto via `$.post('/missao/Logistica/SugestoesCodigoBarras', { sinalGxData: ... })`.

Funções expostas:
- `window.SugestaoCodigoBarras.hidratar(missaoId)` — chamada no init do `item.js` da separação/carregamento; popula chips do estado salvo.

Eventos:
- Click em `.js-sugerir-codigo` / `.js-editar-sugestao` → abre modal com produto atual.
- Click em `[data-action="confirmar"]` → POST `sugerir` → atualiza chip ou mostra erro inline.
- Click em `.js-cancelar-sugestao` → confirm + POST `cancelar` → volta pro botão "Sugerir".

### Integração

Em `separacao/item.php` e `carregamento/item.php`, dentro do template do item:

```php
<?php include __DIR__ . '/../shared/sugestao_codigo_barras.php'; ?>
```

E carregar o JS (uma vez na página):

```php
<script src="<?= base_url() ?>componentes/Missoes/Logistica/sinais/App/html/shared/sugestao_codigo_barras.js"></script>
```

Em `separacao/item.js` e `carregamento/item.js`, no init existente:

```js
if (window.SugestaoCodigoBarras) {
    SugestaoCodigoBarras.hidratar(window.MISSAO_ID);
}
```

## 15. i18n (somente `pt-br.json`)

```json
{
  "sugestao": {
    "botao_sugerir":                  "Sugerir código",
    "label_sugerido":                 "Sugerido",
    "acao_editar":                    "Editar sugestão",
    "acao_cancelar":                  "Cancelar sugestão",
    "modal_titulo":                   "Sugerir código de barras",
    "modal_placeholder":              "Digite ou escaneie o código",
    "confirma_cancelar":              "Cancelar essa sugestão?",
    "erro": {
      "tamanho_minimo":               "Mínimo de {min} caracteres",
      "missao_invalida":              "Missão de origem inválida",
      "produto_invalido":             "Produto inválido",
      "missao_nao_encontrada":        "Missão de origem não encontrada",
      "tipo_origem_invalido":         "Sugestão só permitida em separação/carregamento",
      "missao_ja_finalizada":         "Não é possível sugerir: missão já finalizada",
      "produto_nao_encontrado":       "Produto não encontrado",
      "produto_ja_tem_codigo":        "Este produto já tem código de barras cadastrado",
      "codigo_ja_usado_em_produto":   "Este código já é usado pelo produto: {produto}",
      "codigo_em_outra_sugestao_pendente": "Este código já está em outra sugestão pendente"
    }
  }
}
```

## 16. Testes

Todo trabalho contra `dev_gx_goldie`. Bootstrap padrão Genesis: `$_SESSION` na raiz E em `$_SESSION['dashboard']`.

### Unit

- `Testes/Enums/StatusSugestaoTddTest.php` — `from`, `label`, transições válidas.

### Integration (DB real)

- `Testes/Sondas/MissaoCodigoSugestaoSondaTddTest.php`
  - upsert insere quando não existe
  - upsert atualiza quando pendente existe
  - cancelarPendente muda status + grava `cancelada_em`
  - listarPendentesPorOrigem retorna só pendentes
  - existeOutraPendenteComMesmoCodigo ignora próprio produto + canceladas/consumidas
  - marcarComoConsumidas em lote
- `Testes/Validators/SugerirCodigoBarrasValidatorTddTest.php`
  - rejeita < 6 chars
  - rejeita missão inexistente / finalizada / tipo errado
  - rejeita produto com `codigo_barras` já preenchido
  - rejeita código duplicado em `produtos`
  - rejeita código em outra sugestão pendente (de outro produto)
  - aceita happy path
  - aceita repetir sugestão do MESMO produto (upsert)
- `Testes/Laboratorios/SugerirCodigoBarrasLaboratorioTddTest.php`
  - happy path
  - operador diferente sugere mesmo produto → último ganha
  - cancela + sugere de novo → 2 linhas (1 cancelada + 1 pendente)
- `Testes/Laboratorios/CancelarSugestaoLaboratorioTddTest.php`
  - happy path
  - cancelar já cancelada → erro claro
  - cancelar já consumida → erro
- `Testes/Laboratorios/GerarAjusteDeSugestoesLaboratorioTddTest.php`
  - **0 pendentes → NÃO cria missão** (requisito explícito)
  - N pendentes → 1 missão AJUSTE_DADOS com N itens, status `AGUARDANDO_APROVACAO`
  - título correto pra SEPARACAO e CARREGAMENTO
  - `referencia_tipo` + `referencia_id` apontam pra origem
  - `lider_id` e `operador_id` null
  - cada item tem `dados_adicionais.codigo_barras` (e snapshot)
  - sugestões viram `consumida` com `consumida_em_ajuste_id` linkado
  - canceladas/já consumidas NÃO entram
  - idempotência: rodar 2x não cria 2 missões
- `Adapters/AjusteDados/Testes/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorioTddTest.php`
  - payload passado corretamente
  - snapshot `importacao_id` no `dados_adicionais` (pré-req webhook ERP)
  - erro propagado, não engolido

### Smoke manual

1. Criar separação dev com 2 produtos sem `codigo_barras`.
2. Operador sugere código pra 1 deles.
3. Operador finaliza separação.
4. Assert: existe missão AJUSTE_DADOS em `AGUARDANDO_APROVACAO`, com 1 item, descrição `"Sugestão - Separação #X"`.
5. Repetir cenário com 0 sugestões → NENHUMA missão criada.

## 17. Riscos / atenção

| # | Risco | Mitigação |
|---|---|---|
| 1 | `MissoesAprovacao::sincronizarErpAjusteDados` pode esperar `codigo_barras` em campo específico do `dados_adicionais` | Verificar como a designação manual hoje grava; replicar nome de campo. `dados_adicionais` traz tanto `codigo_barras` quanto `codigo_barras_sugerido` |
| 2 | `class_exists` no hook só funciona se autoload PSR-4 enxergar `SugestoesCodigoBarras` | Sinal segue mesmo padrão de Edicaorota — autoload do Genesis cobre |
| 3 | Operador offline ao clicar "sugerir" | Erro inline no modal; sem buffer local. UX: tenta de novo |
| 4 | Dois operadores na mesma separação editam sugestão do mesmo produto | Last-write-wins (upsert). Extrato grava o último |
| 5 | PRD `hook-ajuste-dados-block` em curso exige `block` no payload do webhook | `dados_adicionais` carrega `importacao_id` (pré-req já documentado). `block` vem da sessão do gestor que aprova |
| 6 | Re-finalização da mesma origem após falha parcial | Guard de idempotência: `GerarAjusteDeSugestoesLaboratorio` verifica se já existe AJUSTE_DADOS com `referencia_id = $origemId` em `AGUARDANDO_APROVACAO` antes de criar |
| 7 | Confusão UX com input de bipagem | Partial só aparece quando `!item.codigo_barras`; visualmente distinta; texto "Sugerir código" |

## 18. Fora de escopo

- Sugestão de outros campos (peso, cubagem, endereço).
- UI pro gestor inspecionar sugestões pendentes antes da finalização da origem.
- Job de retry pra sugestões órfãs caso o callback tenha falhado.
- Notificação push/email pro gestor.
- Internacionalização EN/ES.
- Migrar o hook do core pra event bus genérico (`MissaoEventosRegistry`).

## 19. Ordem de implementação

1. **Migration + Sonda + Enum + testes**.
2. **Validators + Laboratórios sugerir/cancelar/listar + Controller dispatcher + testes**.
3. **Adapter AjusteDados + `GerarAjusteDeSugestoesLaboratorio` + testes integração** (gera ajuste / não gera quando 0 / idempotência).
4. **Hook do core** em `MissoesStatus::concluirOuFinalizarOperador` + smoke test ponta-a-ponta.
5. **Frontend** (partial + JS + includes + strings pt-br).
6. **Verificação manual no banco dev** (`dev-galpaodasembalagens`): fluxo completo → AJUSTE aparece → aprovação → webhook ERP atualiza `produtos.codigo_barras`.

## 20. Métricas de sucesso

- Operador na separação consegue sugerir, editar e cancelar sem refresh.
- Finalizar com N≥1 sugestões → exatamente 1 missão AJUSTE_DADOS em `AGUARDANDO_APROVACAO` com N itens.
- Finalizar com 0 sugestões → zero missões criadas.
- Gestor aprova → webhook ERP atualiza `produtos.codigo_barras`.
- Zero alteração no fluxo de aprovação; alteração mínima no `MissoesStatus` (1 bloco isolado).
