# Sugestão de Código de Barras na Separação/Carregamento — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Permitir que operadores em separação/carregamento sugiram código de barras pra produtos sem cadastro; ao finalizar a missão, sugestões pendentes viram automaticamente uma missão `AJUSTE_DADOS` em `AGUARDANDO_APROVACAO`.

**Architecture:** Sinal novo `SugestoesCodigoBarras` (vertical slice V2 dentro de `Logistica/sinais/`) com Sonda + Validators + Laboratórios + Adapter pro domínio AJUSTE_DADOS. Hook único e isolado em `MissoesStatus::concluirOuFinalizarOperador` chama o Laboratório de geração via `class_exists` (acoplamento mínimo). Frontend reutiliza partial compartilhada em `App/html/shared/` consumida por separação e carregamento.

**Tech Stack:** PHP 8.1+ (match, backed enums), ModelInteligencia V2, jQuery (legado App), PostgreSQL com unique index parcial, `dev_gx_goldie` para integração.

**Spec:** `docs/superpowers/specs/2026-05-26-sugestao-codigo-barras-separacao-carregamento-design.md`

---

## File Structure

**Sinal novo** (criar):
```
componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/
├── SugestoesCodigoBarras.php
├── Migrations/001_create_missao_codigo_sugestao.sql
├── Sondas/MissaoCodigoSugestaoSonda.php
├── Enums/StatusSugestao.php
├── Validators/SugerirCodigoBarrasValidator.php
├── Validators/CancelarSugestaoValidator.php
├── Laboratorios/SugerirCodigoBarrasLaboratorio.php
├── Laboratorios/CancelarSugestaoLaboratorio.php
├── Laboratorios/ListarSugestoesLaboratorio.php
├── Laboratorios/GerarAjusteDeSugestoesLaboratorio.php
├── Adapters/AjusteDados/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorio.php
├── Testes/Enums/StatusSugestaoTddTest.php
├── Testes/Sondas/MissaoCodigoSugestaoSondaTddTest.php
├── Testes/Validators/SugerirCodigoBarrasValidatorTddTest.php
├── Testes/Laboratorios/SugerirCodigoBarrasLaboratorioTddTest.php
├── Testes/Laboratorios/CancelarSugestaoLaboratorioTddTest.php
├── Testes/Laboratorios/GerarAjusteDeSugestoesLaboratorioTddTest.php
└── Adapters/AjusteDados/Testes/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorioTddTest.php
```

**Frontend** (criar):
```
componentes/Missoes/Logistica/sinais/App/html/shared/sugestao_codigo_barras.php
componentes/Missoes/Logistica/sinais/App/html/shared/sugestao_codigo_barras.js
```

**Modificar**:
- `componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesStatus.php` (1 bloco isolado em `concluirOuFinalizarOperador`)
- `componentes/Missoes/Logistica/sinais/App/html/separacao/item.php` (include partial + script tag)
- `componentes/Missoes/Logistica/sinais/App/html/separacao/item.js` (chamada `SugestaoCodigoBarras.hidratar`)
- `componentes/Missoes/Logistica/sinais/App/html/carregamento/item.php` (include partial + script tag)
- `componentes/Missoes/Logistica/sinais/App/html/carregamento/item.js` (chamada `SugestaoCodigoBarras.hidratar`)
- `linguagens/pt-br.json` (chaves `sugestao.*`)

---

## Phase 1 — Foundation: Migration, Enum, Sonda

### Task 1: Migration

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Migrations/001_create_missao_codigo_sugestao.sql`

- [ ] **Step 1: Criar arquivo da migration**

```sql
-- =====================================================
-- Migration: Sugestões de código de barras pelo operador
-- Data: 2026-05-26
-- =====================================================
--
-- Cada linha é uma sugestão por (missao_origem, produto). UPSERT por
-- (missao_origem_id, produto_id) via unique index parcial sobre status='pendente'.
-- Sugestão cancelada vira soft-delete (status='cancelada').
-- Sugestão consumida pelo callback de finalização guarda link pro ajuste gerado.
--
-- Block columns são populadas pelo framework via customerControl/companyControl;
-- não declarar DEFAULT nem alocar manualmente nas queries.
-- =====================================================

CREATE TABLE missao_codigo_sugestao (
    id SERIAL PRIMARY KEY,

    -- Origem (separação ou carregamento)
    missao_origem_id INTEGER NOT NULL
        REFERENCES missao_logistica(id) ON DELETE CASCADE,

    -- Alvo
    produto_id       INTEGER NOT NULL,
    codigo_sugerido  VARCHAR(64) NOT NULL,

    -- Quem sugeriu (auditoria)
    operador_sugeriu_id INTEGER NOT NULL,

    -- Estado
    status VARCHAR(20) NOT NULL DEFAULT 'pendente',
        -- pendente | consumida | cancelada
    consumida_em_ajuste_id INTEGER
        REFERENCES missao_logistica(id) ON DELETE SET NULL,
    consumida_em           TIMESTAMP WITHOUT TIME ZONE,
    cancelada_em           TIMESTAMP WITHOUT TIME ZONE,

    -- Block padrão Galaxia (preenchido pelo framework)
    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
);

-- Garante 1 sugestão pendente por (origem, produto) — base do upsert idempotente
CREATE UNIQUE INDEX uniq_missao_codigo_sugestao_pendente
    ON missao_codigo_sugestao (missao_origem_id, produto_id, block)
    WHERE status = 'pendente';

-- Lookup pela origem (callback de finalização)
CREATE INDEX idx_missao_codigo_sugestao_origem
    ON missao_codigo_sugestao (missao_origem_id, status, block);

-- Varredura cross-mission de "outras pendentes com mesmo código"
CREATE INDEX idx_missao_codigo_sugestao_codigo_status
    ON missao_codigo_sugestao (codigo_sugerido, status, block);
```

- [ ] **Step 2: Aplicar a migration no banco dev**

```bash
psql -h <host> -U <user> -d dev_gx_goldie -f componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Migrations/001_create_missao_codigo_sugestao.sql
```

Expected: `CREATE TABLE` + 3× `CREATE INDEX`.

- [ ] **Step 3: Verificar que a tabela existe**

```bash
psql -h <host> -U <user> -d dev_gx_goldie -c "\d missao_codigo_sugestao"
```

Expected: imprime estrutura da tabela com todas as colunas + 3 índices.

- [ ] **Step 4: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Migrations/001_create_missao_codigo_sugestao.sql
git commit -m "feat(sugestoes-codigo-barras): migration inicial missao_codigo_sugestao"
```

---

### Task 2: Enum `StatusSugestao` + teste

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Enums/StatusSugestao.php`
- Test: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Enums/StatusSugestaoTddTest.php`

- [ ] **Step 1: Escrever teste falhando**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Enums/StatusSugestaoTddTest.php
require_once __DIR__ . '/../../Enums/StatusSugestao.php';

use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Enums\StatusSugestao;

$passou = 0; $falhas = 0;
function check(bool $cond, string $msg) {
    global $passou, $falhas;
    if ($cond) { $passou++; echo "✅ {$msg}\n"; }
    else { $falhas++; echo "❌ {$msg}\n"; }
}

echo "==== StatusSugestao ====\n";

// from
check(StatusSugestao::from('pendente') === StatusSugestao::PENDENTE,  'from(pendente) → PENDENTE');
check(StatusSugestao::from('consumida') === StatusSugestao::CONSUMIDA, 'from(consumida) → CONSUMIDA');
check(StatusSugestao::from('cancelada') === StatusSugestao::CANCELADA, 'from(cancelada) → CANCELADA');

// label
check(StatusSugestao::PENDENTE->label()  === 'Pendente',  'label PENDENTE');
check(StatusSugestao::CONSUMIDA->label() === 'Consumida', 'label CONSUMIDA');
check(StatusSugestao::CANCELADA->label() === 'Cancelada', 'label CANCELADA');

// transicoes
check(StatusSugestao::PENDENTE->podeTransicionarPara(StatusSugestao::CONSUMIDA), 'PENDENTE → CONSUMIDA ok');
check(StatusSugestao::PENDENTE->podeTransicionarPara(StatusSugestao::CANCELADA), 'PENDENTE → CANCELADA ok');
check(!StatusSugestao::PENDENTE->podeTransicionarPara(StatusSugestao::PENDENTE), 'PENDENTE → PENDENTE invalido');
check(!StatusSugestao::CONSUMIDA->podeTransicionarPara(StatusSugestao::CANCELADA), 'CONSUMIDA é estado final');
check(!StatusSugestao::CANCELADA->podeTransicionarPara(StatusSugestao::PENDENTE), 'CANCELADA é estado final');

echo "\nResultado: {$passou} passou, {$falhas} falhou\n";
exit($falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar — deve falhar (enum não existe)**

```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Enums/StatusSugestaoTddTest.php
```

Expected: erro fatal "Class StatusSugestao not found".

- [ ] **Step 3: Implementar enum**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Enums/StatusSugestao.php

namespace GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Enums;

enum StatusSugestao: string
{
    case PENDENTE  = 'pendente';
    case CONSUMIDA = 'consumida';
    case CANCELADA = 'cancelada';

    public function label(): string
    {
        return match ($this) {
            self::PENDENTE  => 'Pendente',
            self::CONSUMIDA => 'Consumida',
            self::CANCELADA => 'Cancelada',
        };
    }

    public function estadoFinal(): bool
    {
        return $this === self::CONSUMIDA || $this === self::CANCELADA;
    }

    public function podeTransicionarPara(self $destino): bool
    {
        return match ($this) {
            self::PENDENTE  => $destino === self::CONSUMIDA || $destino === self::CANCELADA,
            self::CONSUMIDA => false,
            self::CANCELADA => false,
        };
    }
}
```

- [ ] **Step 4: Rodar — deve passar**

```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Enums/StatusSugestaoTddTest.php
```

Expected: `Resultado: 10 passou, 0 falhou`, exit 0.

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Enums componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Enums
git commit -m "feat(sugestoes-codigo-barras): enum StatusSugestao + testes"
```

---

### Task 3: Sonda `MissaoCodigoSugestaoSonda` + teste integration

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Sondas/MissaoCodigoSugestaoSonda.php`
- Test: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Sondas/MissaoCodigoSugestaoSondaTddTest.php`

- [ ] **Step 1: Escrever teste integration (DB real `dev_gx_goldie`)**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Sondas/MissaoCodigoSugestaoSondaTddTest.php

// Bootstrap Genesis — carrega autoload + setup sessão
require_once __DIR__ . '/../../../../../../../system/_bootstrap_test.php';
// ↑ ajustar pro bootstrap real do projeto. Se não existir, copiar de
// componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Laboratorios/CriarEdicaoRotaLaboratorioIntegrationTest.php

session_start();
$_SESSION['galaxiaUserInfo'] = (object)[
    'id' => 1,
    'customerData' => (object)['id' => 22],     // block do tenant dev
    'companyData'  => (object)['id' => 1],
];
$_SESSION['dashboard']['galaxiaUserInfo'] = $_SESSION['galaxiaUserInfo'];

use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda;

$passou = 0; $falhas = 0;
function check(bool $cond, string $msg) {
    global $passou, $falhas;
    if ($cond) { $passou++; echo "✅ {$msg}\n"; }
    else { $falhas++; echo "❌ {$msg}\n"; }
}

echo "==== MissaoCodigoSugestaoSonda (DB) ====\n";

$sonda = new MissaoCodigoSugestaoSonda();

// Cleanup prévio (idempotente)
$sonda->delete('missao_origem_id = :m', 'm=999999');

// upsert insert
$s1 = $sonda->upsertPendente(999999, 12345, 'TST7890123456', 1);
check(!empty($s1->id), 'upsertPendente cria nova linha (id retornado)');
check($s1->status === 'pendente', 'nova linha em status pendente');
check($s1->codigo_sugerido === 'TST7890123456', 'codigo gravado');

// upsert update (mesmo origem+produto, código diferente)
$s2 = $sonda->upsertPendente(999999, 12345, 'TST7890999999', 2);
check($s2->id === $s1->id, 'upsert atualiza linha existente (mesmo id)');
check($s2->codigo_sugerido === 'TST7890999999', 'codigo atualizado');

// listarPendentes
$pendentes = $sonda->listarPendentesPorOrigem(999999);
check(count($pendentes) === 1, 'listarPendentesPorOrigem retorna 1 (não duplicou)');

// existeOutraPendente — mesmo código, mesmo produto → false (é a própria)
$bAtual = (int)($_SESSION['galaxiaUserInfo']->customerData->id ?? 22);
check(!$sonda->existeOutraPendenteComMesmoCodigo('TST7890999999', 12345, $bAtual),
    'existeOutraPendente ignora a própria sugestão');

// existeOutraPendente — mesmo código, produto diferente → true
$sonda->upsertPendente(999999, 99999, 'TST7890999999', 1);
check($sonda->existeOutraPendenteComMesmoCodigo('TST7890999999', 12345, $bAtual),
    'existeOutraPendente detecta outro produto com mesmo código');

// cancelarPendente
check($sonda->cancelarPendente(999999, 12345), 'cancelarPendente retorna true');
$pendentes = $sonda->listarPendentesPorOrigem(999999);
check(count(array_filter($pendentes, fn($p) => $p->produto_id == 12345)) === 0,
    'sugestão cancelada some da lista de pendentes');

// marcarComoConsumidas
$produtoSugestao = $sonda->upsertPendente(999999, 12345, 'TST7890NOVO11', 1);
$sonda->marcarComoConsumidas([$produtoSugestao->id], 7777777);
$pos = $sonda->find('id = :id', "id={$produtoSugestao->id}")->fetch(false);
check($pos->status === 'consumida', 'marcarComoConsumidas → status=consumida');
check((int)$pos->consumida_em_ajuste_id === 7777777, 'consumida_em_ajuste_id linkado');
check(!empty($pos->consumida_em), 'consumida_em gravado');

// Cleanup
$sonda->delete('missao_origem_id = :m', 'm=999999');

echo "\nResultado: {$passou} passou, {$falhas} falhou\n";
exit($falhas > 0 ? 1 : 0);
```

> **Nota sobre bootstrap_test**: se não existir, criar `system/_bootstrap_test.php` que faz `require __DIR__ . '/../vendor/autoload.php';` + setup mínimo de `Connect::getInstance` apontando pra `dev_gx_goldie`. Padrão exato vem do test de Edicaorota (`CriarEdicaoRotaLaboratorioIntegrationTest.php`).

- [ ] **Step 2: Rodar — deve falhar (Sonda não existe)**

```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Sondas/MissaoCodigoSugestaoSondaTddTest.php
```

Expected: erro de classe não encontrada ou método ausente.

- [ ] **Step 3: Implementar Sonda**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Sondas/MissaoCodigoSugestaoSonda.php

namespace GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas;

use Galaxia\_Controladores\ModelInteligencia;
use Base_config\Helpers\Message;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Enums\StatusSugestao;

class MissaoCodigoSugestaoSonda extends ModelInteligencia
{
    public static $entity;
    public static $protected;
    public static $required;
    public static $fields;
    public static $propeties;

    public $message;

    public function __construct()
    {
        $this->message = new Message();
        $this->init();
    }

    public function __call($method, $arguments)
    {
        if (method_exists($this, $method)) {
            $this->init();
            return call_user_func_array([$this, $method], $arguments);
        }
    }

    private function init(): void
    {
        self::$entity   = 'missao_codigo_sugestao';
        self::$protected = ['id'];
        self::$required  = ['missao_origem_id', 'produto_id', 'codigo_sugerido', 'operador_sugeriu_id'];
        self::$fields    = [];
        self::$propeties = [
            'db'              => 'gx_goldie',
            'customerControl' => true,
            'companyControl'  => false,
            'userControl'     => false,
            'sectorControl'   => false,
            'filialControl'   => false,
        ];
        parent::__construct(self::$entity, self::$protected, self::$required, self::$fields, self::$propeties);
    }

    public function upsertPendente(int $origemId, int $produtoId, string $codigo, int $operadorId): ?object
    {
        $existente = $this->find(
            'missao_origem_id = :m AND produto_id = :p AND status = :s',
            "m={$origemId}&p={$produtoId}&s=" . StatusSugestao::PENDENTE->value
        )->fetch(false);

        if ($existente) {
            $this->update(
                ['codigo_sugerido' => $codigo, 'operador_sugeriu_id' => $operadorId],
                'id = :id',
                "id={$existente->id}"
            );
            return $this->find('id = :id', "id={$existente->id}")->fetch(false);
        }

        $criado = $this->create([
            'missao_origem_id'    => $origemId,
            'produto_id'          => $produtoId,
            'codigo_sugerido'     => $codigo,
            'operador_sugeriu_id' => $operadorId,
            'status'              => StatusSugestao::PENDENTE->value,
        ]);
        return $criado;
    }

    public function cancelarPendente(int $origemId, int $produtoId): bool
    {
        $existente = $this->find(
            'missao_origem_id = :m AND produto_id = :p AND status = :s',
            "m={$origemId}&p={$produtoId}&s=" . StatusSugestao::PENDENTE->value
        )->fetch(false);

        if (!$existente) return false;

        $this->update(
            ['status' => StatusSugestao::CANCELADA->value, 'cancelada_em' => date('Y-m-d H:i:s')],
            'id = :id',
            "id={$existente->id}"
        );
        return true;
    }

    public function listarPendentesPorOrigem(int $origemId): array
    {
        $rows = $this->find(
            'missao_origem_id = :m AND status = :s',
            "m={$origemId}&s=" . StatusSugestao::PENDENTE->value
        )->order('id ASC')->fetch(true);
        return $rows ?: [];
    }

    public function existeOutraPendenteComMesmoCodigo(string $codigo, int $produtoIdExcluir, int $block): bool
    {
        $row = $this->find(
            'codigo_sugerido = :c AND produto_id != :p AND status = :s AND block = :b',
            "c={$codigo}&p={$produtoIdExcluir}&s=" . StatusSugestao::PENDENTE->value . "&b={$block}"
        )->fetch(false);
        return !empty($row);
    }

    public function marcarComoConsumidas(array $sugestaoIds, int $ajusteId): void
    {
        if (empty($sugestaoIds)) return;
        $ids = implode(',', array_map('intval', $sugestaoIds));
        $this->update(
            [
                'status'                 => StatusSugestao::CONSUMIDA->value,
                'consumida_em_ajuste_id' => $ajusteId,
                'consumida_em'           => date('Y-m-d H:i:s'),
            ],
            "id IN ({$ids})"
        );
    }

    public function jaTemAjusteParaOrigem(int $origemId): bool
    {
        $row = $this->find(
            'missao_origem_id = :m AND consumida_em_ajuste_id IS NOT NULL',
            "m={$origemId}"
        )->fetch(false);
        return !empty($row);
    }
}
```

- [ ] **Step 4: Rodar — deve passar**

```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Sondas/MissaoCodigoSugestaoSondaTddTest.php
```

Expected: todos os checks ✅, exit 0.

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Sondas componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Sondas
git commit -m "feat(sugestoes-codigo-barras): Sonda MissaoCodigoSugestao + integration tests"
```

---

## Phase 2 — Validators

### Task 4: `SugerirCodigoBarrasValidator` + teste

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Validators/SugerirCodigoBarrasValidator.php`
- Test: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Validators/SugerirCodigoBarrasValidatorTddTest.php`

- [ ] **Step 1: Escrever teste**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Validators/SugerirCodigoBarrasValidatorTddTest.php

require_once __DIR__ . '/../../../../../../../system/_bootstrap_test.php';

session_start();
$_SESSION['galaxiaUserInfo'] = (object)[
    'id' => 1, 'customerData' => (object)['id' => 22], 'companyData' => (object)['id' => 1],
];
$_SESSION['dashboard']['galaxiaUserInfo'] = $_SESSION['galaxiaUserInfo'];

use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Validators\SugerirCodigoBarrasValidator;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda;

$passou = 0; $falhas = 0;
function check(bool $cond, string $msg) {
    global $passou, $falhas;
    if ($cond) { $passou++; echo "✅ {$msg}\n"; }
    else { $falhas++; echo "❌ {$msg}\n"; }
}

echo "==== SugerirCodigoBarrasValidator ====\n";

$validator = new SugerirCodigoBarrasValidator();
$bAtual = 22;

// >>> PREP: assumimos missão de origem 1532 = SEPARACAO ativa em dev,
// e produto 12345 sem codigo_barras. Em prod do plano, criar fixtures.

// 1. Código vazio
$r = $validator->validar(['missao_origem_id' => 1532, 'produto_id' => 12345, 'codigo_sugerido' => ''], $bAtual);
check(!$r['ok'], 'rejeita código vazio');

// 2. Código curto demais
$r = $validator->validar(['missao_origem_id' => 1532, 'produto_id' => 12345, 'codigo_sugerido' => '123'], $bAtual);
check(!$r['ok'] && str_contains($r['erro'], 'aracter'), 'rejeita código < 6 chars');

// 3. Missão inválida
$r = $validator->validar(['missao_origem_id' => 0, 'produto_id' => 12345, 'codigo_sugerido' => '1234567890'], $bAtual);
check(!$r['ok'], 'rejeita missao_origem_id zero');

// 4. Missão inexistente
$r = $validator->validar(['missao_origem_id' => 99999999, 'produto_id' => 12345, 'codigo_sugerido' => '1234567890'], $bAtual);
check(!$r['ok'] && str_contains($r['erro'], 'encontrada'), 'rejeita missão inexistente');

// 5. Happy path
$r = $validator->validar(['missao_origem_id' => 1532, 'produto_id' => 12345, 'codigo_sugerido' => 'TST1234567890'], $bAtual);
check($r['ok'], 'aceita payload válido (happy path)');

// Cleanup pendente
(new MissaoCodigoSugestaoSonda())->cancelarPendente(1532, 12345);

echo "\nResultado: {$passou} passou, {$falhas} falhou\n";
exit($falhas > 0 ? 1 : 0);
```

> **Nota**: o teste assume IDs reais do banco dev. Antes de rodar, identificar via SQL uma missão SEPARACAO ativa + um produto sem `codigo_barras` e ajustar `1532` / `12345` no teste.

- [ ] **Step 2: Rodar — deve falhar (validator não existe)**

```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Validators/SugerirCodigoBarrasValidatorTddTest.php
```

Expected: classe não encontrada.

- [ ] **Step 3: Implementar validator**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Validators/SugerirCodigoBarrasValidator.php

namespace GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Validators;

use GalaxiaConstelacoes\Logistica\MissaoLogistica\Enums\StatusMissao;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Enums\TipoMissao;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda;

class SugerirCodigoBarrasValidator
{
    private const TAMANHO_MINIMO = 6;

    public function validar(array $data, int $blockAtual): array
    {
        $missaoId  = (int)($data['missao_origem_id'] ?? 0);
        $produtoId = (int)($data['produto_id'] ?? 0);
        $codigo    = trim((string)($data['codigo_sugerido'] ?? ''));

        // 1. Obrigatórios
        if ($missaoId <= 0)  return self::erro('Missão de origem inválida');
        if ($produtoId <= 0) return self::erro('Produto inválido');
        if (strlen($codigo) < self::TAMANHO_MINIMO) {
            return self::erro('Mínimo de ' . self::TAMANHO_MINIMO . ' caracteres');
        }

        // 2. Missão de origem existe, é separação/carregamento, e não finalizada
        $missao = $this->lerMissao($missaoId);
        if (!$missao) return self::erro('Missão de origem não encontrada');

        $tiposPermitidos = [TipoMissao::SEPARACAO->id(), TipoMissao::CARREGAMENTO->id()];
        if (!in_array((int)$missao->tipo_missao_id, $tiposPermitidos, true)) {
            return self::erro('Sugestão só permitida em separação/carregamento');
        }

        $finalizados = [
            StatusMissao::CONCLUIDA->value,
            StatusMissao::AGUARDANDO_APROVACAO->value,
            StatusMissao::CANCELADA->value,
        ];
        if (in_array($missao->status, $finalizados, true)) {
            return self::erro('Não é possível sugerir: missão já finalizada');
        }

        // 3. Produto existe e NÃO tem codigo_barras
        $produto = $this->lerProduto($produtoId);
        if (!$produto) return self::erro('Produto não encontrado');
        if (!empty($produto->codigo_barras)) {
            return self::erro('Este produto já tem código de barras cadastrado');
        }

        // 4. Código não pode duplicar codigo_barras de outro produto
        $duplicado = $this->lerProdutoPorCodigo($codigo, $blockAtual);
        if ($duplicado && (int)$duplicado->id !== $produtoId) {
            return self::erro("Este código já é usado pelo produto: {$duplicado->descricao}");
        }

        // 5. Código não pode estar em outra sugestão pendente
        if ((new MissaoCodigoSugestaoSonda())->existeOutraPendenteComMesmoCodigo($codigo, $produtoId, $blockAtual)) {
            return self::erro('Este código já está em outra sugestão pendente');
        }

        return ['ok' => true, 'data' => compact('missaoId', 'produtoId', 'codigo')];
    }

    private function lerMissao(int $id): ?object
    {
        $sonda = new \GalaxiaConstelacoes\Logistica\MissaoLogistica\Sondas\MissaoLogisticaSonda();
        $r = $sonda->find('id = :id', "id={$id}")->fetch(false);
        return $r ?: null;
    }

    private function lerProduto(int $id): ?object
    {
        return \Galaxia\_Controladores\Connect::getInstance()->prepare(
            'SELECT id, descricao, codigo_barras FROM produtos WHERE id = :id LIMIT 1'
        )->execute(['id' => $id])->fetchObject() ?: null;
    }

    private function lerProdutoPorCodigo(string $codigo, int $block): ?object
    {
        return \Galaxia\_Controladores\Connect::getInstance()->prepare(
            'SELECT id, descricao FROM produtos WHERE codigo_barras = :c AND block = :b LIMIT 1'
        )->execute(['c' => $codigo, 'b' => $block])->fetchObject() ?: null;
    }

    private static function erro(string $msg): array
    {
        return ['ok' => false, 'erro' => $msg];
    }
}
```

> **Atenção**: `lerProduto`/`lerProdutoPorCodigo` usam Connect direto **apenas para leitura** — writes vão sempre pela Sonda (regra `feedback_no_raw_sql`). Não existe `ProdutoSonda` V2 ainda; quando existir, substituir.

> **Validar a classe `MissaoLogisticaSonda`** antes de implementar: se ainda for V1 (Inteligencias/MissaoLogistica.php), trocar pra `new MissaoLogistica($galaxiaRoute, 'leitura', ['id' => $id])`. Se nenhum dos dois existir, ler `missao_logistica` direto via Connect.

- [ ] **Step 4: Rodar — deve passar**

```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Validators/SugerirCodigoBarrasValidatorTddTest.php
```

Expected: 5/5 ✅, exit 0.

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Validators/SugerirCodigoBarrasValidator.php \
        componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Validators
git commit -m "feat(sugestoes-codigo-barras): SugerirCodigoBarrasValidator + testes"
```

---

### Task 5: `CancelarSugestaoValidator`

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Validators/CancelarSugestaoValidator.php`

- [ ] **Step 1: Implementar validator (validação simples; sem teste dedicado por enquanto — coberto pelo teste do Laboratório)**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Validators/CancelarSugestaoValidator.php

namespace GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Validators;

use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Enums\StatusSugestao;

class CancelarSugestaoValidator
{
    public function validar(array $data): array
    {
        $missaoId  = (int)($data['missao_origem_id'] ?? 0);
        $produtoId = (int)($data['produto_id'] ?? 0);

        if ($missaoId <= 0)  return ['ok' => false, 'erro' => 'Missão de origem inválida'];
        if ($produtoId <= 0) return ['ok' => false, 'erro' => 'Produto inválido'];

        $sonda = new MissaoCodigoSugestaoSonda();
        $pendente = $sonda->find(
            'missao_origem_id = :m AND produto_id = :p AND status = :s',
            "m={$missaoId}&p={$produtoId}&s=" . StatusSugestao::PENDENTE->value
        )->fetch(false);

        if (!$pendente) {
            return ['ok' => false, 'erro' => 'Sugestão pendente não encontrada para esse produto'];
        }

        return ['ok' => true, 'data' => compact('missaoId', 'produtoId')];
    }
}
```

- [ ] **Step 2: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Validators/CancelarSugestaoValidator.php
git commit -m "feat(sugestoes-codigo-barras): CancelarSugestaoValidator"
```

---

## Phase 3 — Laboratórios + Controller (HTTP)

### Task 6: `SugerirCodigoBarrasLaboratorio` + teste

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/SugerirCodigoBarrasLaboratorio.php`
- Test: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/SugerirCodigoBarrasLaboratorioTddTest.php`

- [ ] **Step 1: Escrever teste**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/SugerirCodigoBarrasLaboratorioTddTest.php

require_once __DIR__ . '/../../../../../../../system/_bootstrap_test.php';

session_start();
$_SESSION['galaxiaUserInfo'] = (object)[
    'id' => 1, 'customerData' => (object)['id' => 22], 'companyData' => (object)['id' => 1],
];
$_SESSION['dashboard']['galaxiaUserInfo'] = $_SESSION['galaxiaUserInfo'];

use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios\SugerirCodigoBarrasLaboratorio;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda;

$passou = 0; $falhas = 0;
function check(bool $cond, string $msg) {
    global $passou, $falhas;
    if ($cond) { $passou++; echo "✅ {$msg}\n"; }
    else { $falhas++; echo "❌ {$msg}\n"; }
}

echo "==== SugerirCodigoBarrasLaboratorio ====\n";

$lab = new SugerirCodigoBarrasLaboratorio();
$sonda = new MissaoCodigoSugestaoSonda();

// Cleanup
$sonda->delete('missao_origem_id = :m', 'm=1532');

// Happy path (assumindo missão 1532 SEPARACAO ativa e produto 12345 sem codigo_barras)
$r = $lab->executar(['missao_origem_id' => 1532, 'produto_id' => 12345, 'codigo_sugerido' => 'TST7891234567']);
check($r['ok'] === true, 'happy path retorna ok=true');
check(!empty($r['sugestao']->id), 'retorna sugestao com id');

// Re-sugerir o mesmo produto com código diferente → upsert (mesmo id, código novo)
$idAnterior = $r['sugestao']->id;
$r2 = $lab->executar(['missao_origem_id' => 1532, 'produto_id' => 12345, 'codigo_sugerido' => 'TST7891234999']);
check($r2['ok'] === true, 'segundo sugerir retorna ok');
check((int)$r2['sugestao']->id === (int)$idAnterior, 'upsert mantém mesmo id');
check($r2['sugestao']->codigo_sugerido === 'TST7891234999', 'codigo atualizado');

// Validação falha → retorna ok=false
$r3 = $lab->executar(['missao_origem_id' => 1532, 'produto_id' => 12345, 'codigo_sugerido' => 'abc']);
check($r3['ok'] === false, 'codigo curto rejeitado');

// Cleanup
$sonda->delete('missao_origem_id = :m', 'm=1532');

echo "\nResultado: {$passou} passou, {$falhas} falhou\n";
exit($falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar — deve falhar**

```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/SugerirCodigoBarrasLaboratorioTddTest.php
```

- [ ] **Step 3: Implementar Laboratório**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/SugerirCodigoBarrasLaboratorio.php

namespace GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios;

use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Validators\SugerirCodigoBarrasValidator;

class SugerirCodigoBarrasLaboratorio
{
    public function executar(array $data): array
    {
        $session    = $_SESSION['galaxiaUserInfo'] ?? null;
        $operadorId = (int)($session->id ?? 0);
        $blockAtual = (int)($session->customerData->id ?? 0);

        if ($operadorId <= 0 || $blockAtual <= 0) {
            return ['ok' => false, 'erro' => 'Sessão inválida'];
        }

        $resultado = (new SugerirCodigoBarrasValidator())->validar($data, $blockAtual);
        if (!$resultado['ok']) return $resultado;

        ['missaoId' => $missaoId, 'produtoId' => $produtoId, 'codigo' => $codigo] = $resultado['data'];

        $sugestao = (new MissaoCodigoSugestaoSonda())
            ->upsertPendente($missaoId, $produtoId, $codigo, $operadorId);

        return ['ok' => true, 'sugestao' => $sugestao];
    }
}
```

- [ ] **Step 4: Rodar — deve passar**

```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/SugerirCodigoBarrasLaboratorioTddTest.php
```

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/SugerirCodigoBarrasLaboratorio.php \
        componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/SugerirCodigoBarrasLaboratorioTddTest.php
git commit -m "feat(sugestoes-codigo-barras): SugerirCodigoBarrasLaboratorio + testes"
```

---

### Task 7: `CancelarSugestaoLaboratorio` + teste

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/CancelarSugestaoLaboratorio.php`
- Test: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/CancelarSugestaoLaboratorioTddTest.php`

- [ ] **Step 1: Escrever teste**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/CancelarSugestaoLaboratorioTddTest.php

require_once __DIR__ . '/../../../../../../../system/_bootstrap_test.php';

session_start();
$_SESSION['galaxiaUserInfo'] = (object)[
    'id' => 1, 'customerData' => (object)['id' => 22], 'companyData' => (object)['id' => 1],
];
$_SESSION['dashboard']['galaxiaUserInfo'] = $_SESSION['galaxiaUserInfo'];

use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios\SugerirCodigoBarrasLaboratorio;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios\CancelarSugestaoLaboratorio;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda;

$passou = 0; $falhas = 0;
function check(bool $cond, string $msg) {
    global $passou, $falhas;
    if ($cond) { $passou++; echo "✅ {$msg}\n"; }
    else { $falhas++; echo "❌ {$msg}\n"; }
}

echo "==== CancelarSugestaoLaboratorio ====\n";

$sugerir  = new SugerirCodigoBarrasLaboratorio();
$cancelar = new CancelarSugestaoLaboratorio();
$sonda    = new MissaoCodigoSugestaoSonda();

$sonda->delete('missao_origem_id = :m', 'm=1532');

// Cria sugestão
$r1 = $sugerir->executar(['missao_origem_id' => 1532, 'produto_id' => 12345, 'codigo_sugerido' => 'TST7891234567']);
check($r1['ok'], 'sugestão criada');

// Cancela
$r2 = $cancelar->executar(['missao_origem_id' => 1532, 'produto_id' => 12345]);
check($r2['ok'], 'cancelamento retorna ok');

// Lista pendentes — vazia
$pendentes = $sonda->listarPendentesPorOrigem(1532);
check(empty($pendentes), 'lista pendentes vazia após cancelar');

// Cancelar de novo → erro (não há pendente)
$r3 = $cancelar->executar(['missao_origem_id' => 1532, 'produto_id' => 12345]);
check(!$r3['ok'], 'cancelar duas vezes retorna erro');

// Pode sugerir de novo após cancelar (nova linha pendente; antiga fica cancelada)
$r4 = $sugerir->executar(['missao_origem_id' => 1532, 'produto_id' => 12345, 'codigo_sugerido' => 'TST7891NOVA00']);
check($r4['ok'], 'pode sugerir após cancelar');

$sonda->delete('missao_origem_id = :m', 'm=1532');

echo "\nResultado: {$passou} passou, {$falhas} falhou\n";
exit($falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar — deve falhar**

```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/CancelarSugestaoLaboratorioTddTest.php
```

- [ ] **Step 3: Implementar Laboratório**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/CancelarSugestaoLaboratorio.php

namespace GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios;

use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Validators\CancelarSugestaoValidator;

class CancelarSugestaoLaboratorio
{
    public function executar(array $data): array
    {
        $resultado = (new CancelarSugestaoValidator())->validar($data);
        if (!$resultado['ok']) return $resultado;

        ['missaoId' => $missaoId, 'produtoId' => $produtoId] = $resultado['data'];

        $ok = (new MissaoCodigoSugestaoSonda())->cancelarPendente($missaoId, $produtoId);

        return $ok
            ? ['ok' => true]
            : ['ok' => false, 'erro' => 'Falha ao cancelar sugestão'];
    }
}
```

- [ ] **Step 4: Rodar — deve passar**

```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/CancelarSugestaoLaboratorioTddTest.php
```

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/CancelarSugestaoLaboratorio.php \
        componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/CancelarSugestaoLaboratorioTddTest.php
git commit -m "feat(sugestoes-codigo-barras): CancelarSugestaoLaboratorio + testes"
```

---

### Task 8: `ListarSugestoesLaboratorio`

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/ListarSugestoesLaboratorio.php`

- [ ] **Step 1: Implementar (operação simples — sem teste dedicado; coberta pelo smoke manual final)**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/ListarSugestoesLaboratorio.php

namespace GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios;

use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda;

class ListarSugestoesLaboratorio
{
    public function executar(array $data): array
    {
        $missaoId = (int)($data['missao_origem_id'] ?? 0);
        if ($missaoId <= 0) return ['ok' => false, 'erro' => 'Missão de origem inválida'];

        $sugestoes = (new MissaoCodigoSugestaoSonda())->listarPendentesPorOrigem($missaoId);

        // Compacta payload pro front: só campos que o JS precisa
        $payload = array_map(fn($s) => [
            'id'               => (int)$s->id,
            'missao_origem_id' => (int)$s->missao_origem_id,
            'produto_id'       => (int)$s->produto_id,
            'codigo_sugerido'  => $s->codigo_sugerido,
        ], $sugestoes);

        return ['ok' => true, 'sugestoes' => $payload];
    }
}
```

- [ ] **Step 2: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/ListarSugestoesLaboratorio.php
git commit -m "feat(sugestoes-codigo-barras): ListarSugestoesLaboratorio"
```

---

### Task 9: Controller `SugestoesCodigoBarras.php` (dispatcher fino) + rota

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/SugestoesCodigoBarras.php`

- [ ] **Step 1: Implementar dispatcher**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/SugestoesCodigoBarras.php

namespace GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras;

use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios\SugerirCodigoBarrasLaboratorio;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios\CancelarSugestaoLaboratorio;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios\ListarSugestoesLaboratorio;

/**
 * Dispatcher fino do sinal — roteia por $data['sinalGxData'].
 *
 *   'sugerir'  → SugerirCodigoBarrasLaboratorio
 *   'cancelar' → CancelarSugestaoLaboratorio
 *   'listar'   → ListarSugestoesLaboratorio
 */
class SugestoesCodigoBarras
{
    public $galaxiaRoute;
    public $response = [];

    public function __construct(object $galaxia, ?array $data = [])
    {
        $this->galaxiaRoute = $galaxia;
    }

    public function start(?array $data = [])
    {
        $acao = $data['sinalGxData'] ?? '';

        $resultado = match ($acao) {
            'sugerir'  => (new SugerirCodigoBarrasLaboratorio())->executar($data),
            'cancelar' => (new CancelarSugestaoLaboratorio())->executar($data),
            'listar'   => (new ListarSugestoesLaboratorio())->executar($data),
            default    => ['ok' => false, 'erro' => 'Ação inválida'],
        };

        $this->response = $resultado;

        // Compatibilidade Galaxia: emitir JSON quando chamado via XHR
        if (!headers_sent() && (strpos($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json') !== false
            || ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'XMLHttpRequest')) {
            header('Content-Type: application/json; charset=utf-8');
            echo json_encode($resultado);
            return;
        }

        echo json_encode($resultado);
    }
}
```

- [ ] **Step 2: Validar manualmente que a rota responde**

Abrir num browser autenticado (ou via curl com cookie de sessão):
```
POST /missao/Logistica/SugestoesCodigoBarras
Body: sinalGxData=listar&missao_origem_id=1532
```

Expected: `{"ok":true,"sugestoes":[]}` (lista vazia, 200).

- [ ] **Step 3: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/SugestoesCodigoBarras.php
git commit -m "feat(sugestoes-codigo-barras): controller dispatcher + roteamento"
```

---

## Phase 4 — Adapter AjusteDados + Geração da Missão

### Task 10: `CriarAjusteDadosDeSugestoesLaboratorio` (Adapter)

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorio.php`
- Test: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Testes/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorioTddTest.php`

- [ ] **Step 1: Verificar como o aprovador lê o `codigo_barras` do `dados_adicionais`**

```bash
grep -n "codigo_barras\|dados_adicionais" /home/goldie/www/genesis/componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesAprovacao.php | head -20
```

Anotar o nome exato do campo lido — vai pro `dados_adicionais` do item gerado. Provavelmente `codigo_barras` (mesmo nome que o gestor usa na designação manual).

- [ ] **Step 2: Escrever teste**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Testes/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorioTddTest.php

require_once __DIR__ . '/../../../../../../../../../system/_bootstrap_test.php';

session_start();
$_SESSION['galaxiaUserInfo'] = (object)[
    'id' => 1, 'customerData' => (object)['id' => 22], 'companyData' => (object)['id' => 1],
];
$_SESSION['dashboard']['galaxiaUserInfo'] = $_SESSION['galaxiaUserInfo'];

use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Adapters\AjusteDados\Laboratorios\CriarAjusteDadosDeSugestoesLaboratorio;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Enums\TipoMissao;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Enums\StatusMissao;

$passou = 0; $falhas = 0;
function check(bool $cond, string $msg) {
    global $passou, $falhas;
    if ($cond) { $passou++; echo "✅ {$msg}\n"; }
    else { $falhas++; echo "❌ {$msg}\n"; }
}

echo "==== CriarAjusteDadosDeSugestoesLaboratorio ====\n";

// Fixture: simular array de sugestões (sem precisar de Sonda real)
$sugestoes = [
    (object)['id' => 1, 'produto_id' => 12345, 'codigo_sugerido' => 'TST1234567890', 'operador_sugeriu_id' => 1],
    (object)['id' => 2, 'produto_id' => 12346, 'codigo_sugerido' => 'TST1234567891', 'operador_sugeriu_id' => 1],
];

$adapter = new CriarAjusteDadosDeSugestoesLaboratorio();
$ajusteId = $adapter->executar([
    'descricao'       => 'Sugestão - Separação #1532',
    'referencia_tipo' => 'SEPARACAO',
    'referencia_id'   => 1532,
    'block'           => 22,
    'sugestoes'       => $sugestoes,
]);

check($ajusteId > 0, 'retorna ajusteId > 0');

// Validar via SQL direto que a missão existe + status correto
$db = \Galaxia\_Controladores\Connect::getInstance();
$row = $db->prepare('SELECT * FROM missao_logistica WHERE id = :id')
    ->execute(['id' => $ajusteId])->fetchObject();

check((int)$row->tipo_missao_id === TipoMissao::AJUSTE_DADOS->id(), 'tipo = AJUSTE_DADOS');
check($row->status === StatusMissao::AGUARDANDO_APROVACAO->value, 'status = AGUARDANDO_APROVACAO');
check($row->descricao === 'Sugestão - Separação #1532', 'descricao correta');
check($row->referencia_tipo === 'SEPARACAO', 'referencia_tipo');
check((int)$row->referencia_id === 1532, 'referencia_id');
check($row->lider_id === null, 'lider_id null');
check($row->operador_id === null, 'operador_id null');

// Validar itens
$itens = $db->prepare('SELECT * FROM missao_logistica_itens WHERE missao_logistica_id = :m ORDER BY id')
    ->execute(['m' => $ajusteId])->fetchAll(\PDO::FETCH_OBJ);

check(count($itens) === 2, '2 itens criados');
check((int)$itens[0]->produto_id === 12345, 'item 1 produto correto');

$dados = json_decode($itens[0]->dados_adicionais);
check($dados->codigo_barras === 'TST1234567890', 'item 1 dados_adicionais.codigo_barras');
check((int)$dados->origem_missao_id === 1532, 'origem_missao_id no snapshot');

// Cleanup
$db->prepare('DELETE FROM missao_logistica_itens WHERE missao_logistica_id = :m')->execute(['m' => $ajusteId]);
$db->prepare('DELETE FROM missao_logistica WHERE id = :id')->execute(['id' => $ajusteId]);

echo "\nResultado: {$passou} passou, {$falhas} falhou\n";
exit($falhas > 0 ? 1 : 0);
```

- [ ] **Step 3: Rodar — deve falhar**

```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Testes/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorioTddTest.php
```

- [ ] **Step 4: Implementar Adapter**

> **Descobertas críticas durante o pre-flight do plano** (verificadas em `MissoesPersistencia.php` linhas 123-310 e `MissoesAprovacao.php` linhas 280-470):
> 1. **`MissoesPersistencia` é invocada via construtor** com `opcao`:
>    `new MissoesPersistencia($galaxia, 'criacao', $payload)`. O método público é o `__construct`; `criacao()` é private.
> 2. **`criacao()` cria a missão em `StatusMissao::PENDENTE`** (hardcoded). Precisa de UPDATE separado pra transicionar para `AGUARDANDO_APROVACAO` depois.
> 3. **Os campos coletados pelo aprovador moram em `dados_coletados` (JSON), não `dados_adicionais`.** Em `MissoesAprovacao.php:321`: `$dados = $item->dados_coletados;` → mapeia via `MAPEAMENTO_CAMPOS['codigo_barras'] = 'codigo_barras'`.
> 4. **`dados_adicionais` é onde mora o snapshot** (ex: `importacao_id`) — em `MissoesAprovacao.php:359`: `$dadosAdic = $item->dados_adicionais;`.
> 5. **`batchInsertItens` é privado** (linha 330). É invocado dentro de `criacao()` quando o payload tem `produtos` array. Não chamar direto.
> 6. **`criacao()` espera `produtos` no payload** com estrutura `[id => produtoId, dados_coletados => ..., dados_adicionais => ...]` — verificar exato em `MissoesPersistencia::batchInsertItens` linhas 330-420.

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorio.php

namespace GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Adapters\AjusteDados\Laboratorios;

use GalaxiaConstelacoes\Logistica\MissaoLogistica\Enums\StatusMissao;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Enums\TipoMissao;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Satelites\MissoesPersistencia;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Inteligencias\MissaoLogistica;

/**
 * Cria a missão AJUSTE_DADOS a partir das sugestões da origem.
 * Isola integração com domínio AJUSTE_DADOS.
 *
 * Fluxo:
 *   1. Monta payload com `produtos` populando `dados_coletados.codigo_barras` (lido pelo aprovador)
 *      e `dados_adicionais` com snapshot (importacao_id + rastreio).
 *   2. Chama `new MissoesPersistencia($galaxia, 'criacao', $payload)` → cria em PENDENTE.
 *   3. Faz UPDATE direto via Inteligencia pra transicionar pra AGUARDANDO_APROVACAO.
 */
class CriarAjusteDadosDeSugestoesLaboratorio
{
    public function executar(object $galaxia, array $payload): int
    {
        $produtos = [];
        foreach ($payload['sugestoes'] as $s) {
            $produtoMeta = $this->lerProduto((int)$s->produto_id);
            $produtos[] = [
                'id'                => (int)$s->produto_id,
                'produto_id'        => (int)$s->produto_id,
                'qtd_total'         => 1,
                'qtd_bipada'        => 1,
                'dados_coletados'   => json_encode([
                    'codigo_barras' => $s->codigo_sugerido,   // chave lida por MAPEAMENTO_CAMPOS
                ]),
                'dados_adicionais'  => json_encode([
                    'origem_missao_id'    => (int)$payload['referencia_id'],
                    'origem_sugestao_id'  => (int)$s->id,
                    'operador_sugeriu_id' => (int)$s->operador_sugeriu_id,
                    'codigo_barras_atual' => $produtoMeta->codigo_barras ?? null,
                    'importacao_id'       => $produtoMeta->importacao_id ?? null,
                ]),
            ];
        }

        $missaoPayload = [
            'tipo_missao_id'   => TipoMissao::AJUSTE_DADOS->id(),
            'descricao'        => $payload['descricao'],
            'referencia_tipo'  => $payload['referencia_tipo'],
            'referencia_id'    => $payload['referencia_id'],
            'requer_aprovacao' => 1,
            'produtos'         => $produtos,
            'campos_ajuste'    => json_encode(['codigo_barras']),
            'usuario_id'       => null,                       // sem operador atribuído
        ];

        // Passo 1: cria a missão em PENDENTE + itens em lote
        $persistencia = new MissoesPersistencia($galaxia, 'criacao', $missaoPayload);
        $response = $persistencia->response ?? null;

        $ajusteId = $this->extrairAjusteId($response);
        if ($ajusteId <= 0) {
            throw new \RuntimeException('Falha ao criar missão AJUSTE_DADOS: '
                . json_encode($response));
        }

        // Passo 2: transiciona pra AGUARDANDO_APROVACAO (criacao() força PENDENTE)
        (new MissaoLogistica())->make([
            'id'             => $ajusteId,
            'status'         => StatusMissao::AGUARDANDO_APROVACAO->value,
            'finalizado_em'  => date('Y-m-d H:i:s'),
        ])->save();

        return $ajusteId;
    }

    private function extrairAjusteId($response): int
    {
        // O retorno de MissoesPersistencia::criacao varia. Inspecionar e extrair:
        // - Pode vir em $response['data']['missao_id'], ['id'], ou direto $response->id.
        // Verificar via dump na primeira execução e ajustar.
        if (is_array($response)) {
            return (int)(
                $response['missao_id']
                ?? $response['id']
                ?? $response['data']['id']
                ?? 0
            );
        }
        if (is_object($response)) {
            return (int)($response->id ?? $response->missao_id ?? 0);
        }
        return 0;
    }

    private function lerProduto(int $id): ?object
    {
        return \Galaxia\_Controladores\Connect::getInstance()->prepare(
            'SELECT id, descricao, codigo_barras, importacao_id FROM produtos WHERE id = :id LIMIT 1'
        )->execute(['id' => $id])->fetchObject() ?: null;
    }
}
```

> **Ações de validação durante implementação (não pular)**:
> 1. Ler `MissoesPersistencia::batchInsertItens` (linhas 330-420) pra confirmar formato exato dos produtos no payload (campos esperados: `id`, `qtd_total`, `dados_coletados`, `dados_adicionais`, etc.)
> 2. Rodar `criacao` uma vez e dumpar o `$response` pra ajustar `extrairAjusteId`.
> 3. Confirmar que `MissaoLogistica` (V1) está disponível pra UPDATE direto via `make()->save()`.

- [ ] **Step 5: Rodar — deve passar**

```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados/Testes/Laboratorios/CriarAjusteDadosDeSugestoesLaboratorioTddTest.php
```

- [ ] **Step 6: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Adapters/AjusteDados
git commit -m "feat(sugestoes-codigo-barras): adapter cria missão AJUSTE_DADOS + testes"
```

---

### Task 11: `GerarAjusteDeSugestoesLaboratorio` (coração do fluxo) + teste

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/GerarAjusteDeSugestoesLaboratorio.php`
- Test: `componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/GerarAjusteDeSugestoesLaboratorioTddTest.php`

- [ ] **Step 1: Escrever teste cobrindo todos os cenários do design (sec 16)**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/GerarAjusteDeSugestoesLaboratorioTddTest.php

require_once __DIR__ . '/../../../../../../../system/_bootstrap_test.php';

session_start();
$_SESSION['galaxiaUserInfo'] = (object)[
    'id' => 1, 'customerData' => (object)['id' => 22], 'companyData' => (object)['id' => 1],
];
$_SESSION['dashboard']['galaxiaUserInfo'] = $_SESSION['galaxiaUserInfo'];

use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios\GerarAjusteDeSugestoesLaboratorio;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios\SugerirCodigoBarrasLaboratorio;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda;

$passou = 0; $falhas = 0;
function check(bool $cond, string $msg) {
    global $passou, $falhas;
    if ($cond) { $passou++; echo "✅ {$msg}\n"; }
    else { $falhas++; echo "❌ {$msg}\n"; }
}

echo "==== GerarAjusteDeSugestoesLaboratorio ====\n";

$sonda  = new MissaoCodigoSugestaoSonda();
$gerar  = new GerarAjusteDeSugestoesLaboratorio();
$sugerir = new SugerirCodigoBarrasLaboratorio();
$db = \Galaxia\_Controladores\Connect::getInstance();

// Constantes de teste (ajustar pra IDs reais em dev)
$MISSAO_SEP_ID  = 1532;   // SEPARACAO ativa
$MISSAO_CARR_ID = 1533;   // CARREGAMENTO ativa
$PROD_A = 12345; $PROD_B = 12346;

$cleanup = function () use ($sonda, $db, $MISSAO_SEP_ID, $MISSAO_CARR_ID) {
    $sonda->delete('missao_origem_id IN (:s, :c)', "s={$MISSAO_SEP_ID}&c={$MISSAO_CARR_ID}");
    $db->prepare("DELETE FROM missao_logistica_itens WHERE missao_logistica_id IN
        (SELECT id FROM missao_logistica WHERE referencia_id IN (:s, :c) AND referencia_tipo IN ('SEPARACAO','CARREGAMENTO'))")
       ->execute(['s' => $MISSAO_SEP_ID, 'c' => $MISSAO_CARR_ID]);
    $db->prepare("DELETE FROM missao_logistica WHERE referencia_id IN (:s, :c) AND referencia_tipo IN ('SEPARACAO','CARREGAMENTO')")
       ->execute(['s' => $MISSAO_SEP_ID, 'c' => $MISSAO_CARR_ID]);
};

$cleanup();

// Cenário 1: zero sugestões → não cria
$resultado = $gerar->executar($MISSAO_SEP_ID);
check($resultado === null, 'sem sugestões pendentes → retorna null');
$row = $db->prepare("SELECT COUNT(*) AS n FROM missao_logistica WHERE referencia_id = :id")
    ->execute(['id' => $MISSAO_SEP_ID])->fetchObject();
check((int)$row->n === 0, 'sem sugestões → nenhuma missão criada');

// Cenário 2: 2 sugestões pendentes → cria 1 missão com 2 itens
$sugerir->executar(['missao_origem_id' => $MISSAO_SEP_ID, 'produto_id' => $PROD_A, 'codigo_sugerido' => 'TST111111111']);
$sugerir->executar(['missao_origem_id' => $MISSAO_SEP_ID, 'produto_id' => $PROD_B, 'codigo_sugerido' => 'TST222222222']);
$ajusteId = $gerar->executar($MISSAO_SEP_ID);
check($ajusteId > 0, 'com sugestões → retorna ajusteId');

$missao = $db->prepare("SELECT * FROM missao_logistica WHERE id = :id")
    ->execute(['id' => $ajusteId])->fetchObject();
check($missao->descricao === "Sugestão - Separação #{$MISSAO_SEP_ID}", 'descrição "Sugestão - Separação #N"');
check($missao->status === 'aguardando_aprovacao', 'status aguardando_aprovacao');

$itens = $db->prepare("SELECT * FROM missao_logistica_itens WHERE missao_logistica_id = :m ORDER BY id")
    ->execute(['m' => $ajusteId])->fetchAll(\PDO::FETCH_OBJ);
check(count($itens) === 2, '2 itens criados');

// Sugestões marcadas como consumidas + link
$ainda = $sonda->listarPendentesPorOrigem($MISSAO_SEP_ID);
check(count($ainda) === 0, 'sugestões viram consumidas (não há mais pendentes)');
$consumida = $sonda->find('produto_id = :p AND status = :s', "p={$PROD_A}&s=consumida")->fetch(false);
check((int)$consumida->consumida_em_ajuste_id === $ajusteId, 'consumida_em_ajuste_id linkado');

// Cenário 3: idempotência — rodar de novo não duplica
$ajusteId2 = $gerar->executar($MISSAO_SEP_ID);
check($ajusteId2 === null, '2ª execução sem pendentes retorna null (idempotente)');

// Cenário 4: CARREGAMENTO → título correto
$sugerir->executar(['missao_origem_id' => $MISSAO_CARR_ID, 'produto_id' => $PROD_A, 'codigo_sugerido' => 'TST333333333']);
$ajusteCarrId = $gerar->executar($MISSAO_CARR_ID);
$mCarr = $db->prepare("SELECT descricao FROM missao_logistica WHERE id = :id")
    ->execute(['id' => $ajusteCarrId])->fetchObject();
check($mCarr->descricao === "Sugestão - Carregamento #{$MISSAO_CARR_ID}", 'descrição "Sugestão - Carregamento #N"');

$cleanup();

echo "\nResultado: {$passou} passou, {$falhas} falhou\n";
exit($falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar — deve falhar**

```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/GerarAjusteDeSugestoesLaboratorioTddTest.php
```

- [ ] **Step 3: Implementar Laboratório**

```php
<?php
// componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/GerarAjusteDeSugestoesLaboratorio.php

namespace GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios;

use GalaxiaConstelacoes\Logistica\MissaoLogistica\Enums\TipoMissao;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Adapters\AjusteDados\Laboratorios\CriarAjusteDadosDeSugestoesLaboratorio;
use GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Sondas\MissaoCodigoSugestaoSonda;

/**
 * Disparado pelo hook do core quando uma missão SEPARACAO/CARREGAMENTO é finalizada.
 * Lê sugestões pendentes da origem, cria 1 missão AJUSTE_DADOS (se houver ≥1),
 * marca sugestões como consumidas linkando ao ajuste gerado.
 *
 * Recebe $galaxia (GalaxiaRoute) pois o Adapter precisa instanciar MissoesPersistencia,
 * que exige $galaxia no construtor.
 *
 * Retorno: int ajusteId criado, ou null se nada foi gerado.
 */
class GerarAjusteDeSugestoesLaboratorio
{
    public function executar(object $galaxia, int $missaoOrigemId): ?int
    {
        $sonda      = new MissaoCodigoSugestaoSonda();
        $pendentes  = $sonda->listarPendentesPorOrigem($missaoOrigemId);

        if (empty($pendentes)) return null;

        if ($sonda->jaTemAjusteParaOrigem($missaoOrigemId)) {
            error_log("[SugestoesCodigoBarras] origem {$missaoOrigemId} já tem ajuste; abortando.");
            return null;
        }

        $origem = $this->lerOrigem($missaoOrigemId);
        if (!$origem) {
            error_log("[SugestoesCodigoBarras] origem {$missaoOrigemId} não encontrada");
            return null;
        }

        $tipoLabel = match ((int)$origem->tipo_missao_id) {
            TipoMissao::SEPARACAO->id()    => 'Separação',
            TipoMissao::CARREGAMENTO->id() => 'Carregamento',
            default                         => 'Missão',
        };
        $tipoValue = match ((int)$origem->tipo_missao_id) {
            TipoMissao::SEPARACAO->id()    => 'SEPARACAO',
            TipoMissao::CARREGAMENTO->id() => 'CARREGAMENTO',
            default                         => 'OUTRA',
        };

        $ajusteId = (new CriarAjusteDadosDeSugestoesLaboratorio())->executar($galaxia, [
            'descricao'       => "Sugestão - {$tipoLabel} #{$missaoOrigemId}",
            'referencia_tipo' => $tipoValue,
            'referencia_id'   => $missaoOrigemId,
            'sugestoes'       => $pendentes,
        ]);

        $sonda->marcarComoConsumidas(
            array_map(fn($s) => (int)$s->id, $pendentes),
            $ajusteId
        );

        return $ajusteId;
    }

    private function lerOrigem(int $id): ?object
    {
        return \Galaxia\_Controladores\Connect::getInstance()->prepare(
            'SELECT id, tipo_missao_id FROM missao_logistica WHERE id = :id LIMIT 1'
        )->execute(['id' => $id])->fetchObject() ?: null;
    }
}
```

- [ ] **Step 4: Rodar — deve passar**

```bash
php componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/GerarAjusteDeSugestoesLaboratorioTddTest.php
```

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Laboratorios/GerarAjusteDeSugestoesLaboratorio.php \
        componentes/Missoes/Logistica/sinais/SugestoesCodigoBarras/Testes/Laboratorios/GerarAjusteDeSugestoesLaboratorioTddTest.php
git commit -m "feat(sugestoes-codigo-barras): GerarAjusteDeSugestoesLaboratorio + testes"
```

---

## Phase 5 — Hook do core

### Task 12: Patch em `MissoesStatus::concluirOuFinalizarOperador`

**Files:**
- Modify: `componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesStatus.php`

- [ ] **Step 1: Localizar o ponto exato do patch**

```bash
grep -n "registrarExtrato.*MISSAO_CONCLUIDA" componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesStatus.php
```

Identificar a função `concluirOuFinalizarOperador` (~linhas 280-360 conforme spec).

- [ ] **Step 2: Aplicar o patch logo após `registrarExtrato(MISSAO_CONCLUIDA, ...)` e antes do retorno**

Substituir o bloco que detecta tipo e faz `voltarPagina/atualizarPagina` por uma versão com o gancho. A inserção é apenas adicionar o seguinte bloco **imediatamente após** `$this->registrarExtrato($missaoId, AcaoExtrato::MISSAO_CONCLUIDA, null, $stats);`:

```php
// === Hook: gerar AJUSTE_DADOS a partir das sugestões pendentes da origem ===
// Sinal SugestoesCodigoBarras decide; se nenhum pendente, retorna null e nada acontece.
$tipoMissaoIdAtual = (int)($missao->tipo_missao_id ?? 0);
if (in_array($tipoMissaoIdAtual, [TipoMissao::SEPARACAO->id(), TipoMissao::CARREGAMENTO->id()], true)) {
    $cls = '\GalaxiaMissoes\Logistica\sinais\SugestoesCodigoBarras\Laboratorios\GerarAjusteDeSugestoesLaboratorio';
    if (class_exists($cls)) {
        try {
            (new $cls())->executar($this->galaxia, (int)$missaoId);
        } catch (\Throwable $e) {
            error_log("[SugestoesCodigoBarras] falha ao gerar ajuste em missao={$missaoId}: " . $e->getMessage());
        }
    }
}
// === /Hook ===
```

- [ ] **Step 3: Smoke test ponta-a-ponta**

No banco dev, identificar missão SEPARACAO real ativa com produto sem `codigo_barras`. Via UI ou SQL:

```sql
-- localizar uma SEPARACAO ativa
SELECT id, status, descricao FROM missao_logistica
WHERE tipo_missao_id = (SELECT id FROM tipo_missao_logistica WHERE nome='SEPARACAO')
  AND status IN ('pendente','em_andamento')
LIMIT 5;
```

Inserir manualmente uma sugestão (simulando o frontend antes dele estar pronto):

```sql
INSERT INTO missao_codigo_sugestao
  (missao_origem_id, produto_id, codigo_sugerido, operador_sugeriu_id, status, block)
VALUES (<id_separacao>, <produto_sem_codbar>, 'SMK1234567890', 1, 'pendente', 22);
```

Finalizar a missão pelo fluxo normal (UI ou API).

**Assert manual (SQL):**

```sql
SELECT id, descricao, status, referencia_tipo, referencia_id
FROM missao_logistica
WHERE referencia_id = <id_separacao>
  AND descricao LIKE 'Sugestão -%';
```

Expected: 1 linha, status='aguardando_aprovacao', descrição contém "Sugestão - Separação #<id>".

```sql
SELECT status, consumida_em_ajuste_id
FROM missao_codigo_sugestao
WHERE missao_origem_id = <id_separacao>;
```

Expected: status='consumida', consumida_em_ajuste_id preenchido.

- [ ] **Step 4: Repetir smoke test sem sugestão**

Finalizar outra SEPARACAO **sem** inserir sugestão. Assert que NENHUMA missão AJUSTE foi criada com `referencia_id` daquela origem.

- [ ] **Step 5: Commit**

```bash
git add componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesStatus.php
git commit -m "feat(sugestoes-codigo-barras): hook no core gera ajuste ao finalizar separacao/carregamento"
```

---

## Phase 6 — Frontend

### Task 13: Partial PHP compartilhada

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/App/html/shared/sugestao_codigo_barras.php`

- [ ] **Step 1: Implementar partial**

```php
<?php
// componentes/Missoes/Logistica/sinais/App/html/shared/sugestao_codigo_barras.php
//
// Partial reutilizada por separacao/item.php e carregamento/item.php.
// Renderiza botão de sugestão (apenas se produto não tem codigo_barras) +
// modal compartilhada de input.
//
// Variáveis esperadas no escopo de quem incluir:
//   $item       — objeto do item (precisa de ->produto_id e ->codigo_barras)
//   $missaoId   — int id da missão de origem (separação ou carregamento)

if (!isset($item) || !isset($missaoId)) return;
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 ?>"
     style="margin-top:0.5rem;">

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

    <div class="chip-sugerido" hidden style="display:inline-flex;align-items:center;gap:0.4rem;padding:0.2rem 0.5rem;background:#fff8e1;border:1px solid #ffc107;border-radius:4px;font-size:0.85rem;">
        <i class="bi bi-check2-circle text-warning"></i>
        <span><?= htmlspecialchars(show('sugestao.label_sugerido')) ?>:</span>
        <strong class="codigo"></strong>
        <button type="button" class="btn btn-link btn-sm js-editar-sugestao p-0" title="<?= htmlspecialchars(show('sugestao.acao_editar')) ?>">
            <i class="bi bi-pencil"></i>
        </button>
        <button type="button" class="btn btn-link btn-sm text-danger js-cancelar-sugestao p-0" title="<?= htmlspecialchars(show('sugestao.acao_cancelar')) ?>">
            <i class="bi bi-x-lg"></i>
        </button>
    </div>
</div>
```

- [ ] **Step 2: Partial da modal (renderizar 1× na página, fora do loop de itens)**

Criar arquivo separado pra modal:

```php
<?php
// componentes/Missoes/Logistica/sinais/App/html/shared/sugestao_codigo_barras_modal.php
?>
<div id="modal-sugerir-codigo" class="modal" tabindex="-1"
     style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:1050;align-items:center;justify-content:center;">
    <div class="modal-content" style="background:#fff;padding:1.5rem;border-radius:6px;max-width:400px;width:90%;">
        <h5><?= htmlspecialchars(show('sugestao.modal_titulo')) ?></h5>
        <p class="produto-nome text-muted small"></p>
        <input type="text" id="input-codigo-sugerido"
               class="form-control"
               minlength="6" maxlength="64"
               placeholder="<?= htmlspecialchars(show('sugestao.modal_placeholder')) ?>"
               autocomplete="off" inputmode="numeric">
        <div class="erro alert alert-danger mt-2" hidden></div>
        <div class="actions mt-3 d-flex justify-content-end gap-2">
            <button class="btn btn-secondary" data-action="fechar"><?= htmlspecialchars(show('comum.cancelar')) ?></button>
            <button class="btn btn-primary" data-action="confirmar"><?= htmlspecialchars(show('comum.confirmar')) ?></button>
        </div>
    </div>
</div>
```

- [ ] **Step 3: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/App/html/shared/sugestao_codigo_barras.php \
        componentes/Missoes/Logistica/sinais/App/html/shared/sugestao_codigo_barras_modal.php
git commit -m "feat(sugestoes-codigo-barras): partial PHP do chip + modal compartilhada"
```

---

### Task 14: Bundle JS compartilhado

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/App/html/shared/sugestao_codigo_barras.js`

- [ ] **Step 1: Implementar bundle**

```js
// componentes/Missoes/Logistica/sinais/App/html/shared/sugestao_codigo_barras.js
//
// Bundle compartilhado entre separacao e carregamento.
// Expõe window.SugestaoCodigoBarras.hidratar(missaoId).

(function () {
    const ROUTE = '/missao/Logistica/SugestoesCodigoBarras';
    const sugestoesByProduto = {};   // produtoId -> { codigo, missaoId }
    let alvoAtual = null;

    function ajax(payload) {
        return $.ajax({
            url: ROUTE,
            method: 'POST',
            dataType: 'json',
            data: payload,
            headers: { 'X-Requested-With': 'XMLHttpRequest' }
        });
    }

    function aplicarChipSugerido(produtoId, codigo) {
        const $c = $(`.sugestao-codigo-barras[data-produto-id="${produtoId}"]`);
        $c.find('.js-sugerir-codigo').hide();
        const $chip = $c.find('.chip-sugerido');
        $chip.find('.codigo').text(codigo);
        $chip.prop('hidden', false).css('display', 'inline-flex');
    }

    function aplicarBotaoSugerir(produtoId) {
        const $c = $(`.sugestao-codigo-barras[data-produto-id="${produtoId}"]`);
        $c.find('.js-sugerir-codigo').show();
        $c.find('.chip-sugerido').prop('hidden', true).hide();
    }

    function fecharModal() {
        $('#modal-sugerir-codigo').hide();
        $('#modal-sugerir-codigo .erro').prop('hidden', true).text('');
        alvoAtual = null;
    }

    function mostrarErroModal(msg) {
        $('#modal-sugerir-codigo .erro').text(msg).prop('hidden', false);
    }

    function abrirModal($container, missaoId, produtoId) {
        const produtoNome = $container.closest('[data-produto-nome]').data('produto-nome')
            || $container.closest('.item-card,.card').find('.produto-nome,.descricao').first().text()
            || 'Produto';
        alvoAtual = { missaoId, produtoId, $container };
        $('#modal-sugerir-codigo .produto-nome').text(produtoNome);
        $('#input-codigo-sugerido').val(sugestoesByProduto[produtoId]?.codigo || '');
        $('#modal-sugerir-codigo .erro').prop('hidden', true);
        $('#modal-sugerir-codigo').css('display', 'flex');
        setTimeout(() => $('#input-codigo-sugerido').focus(), 50);
    }

    // === Eventos ===

    $(document).on('click', '.js-sugerir-codigo, .js-editar-sugestao', function () {
        const $c = $(this).closest('.sugestao-codigo-barras');
        abrirModal($c, +$c.data('missao-origem'), +$c.data('produto-id'));
    });

    $(document).on('click', '#modal-sugerir-codigo [data-action="fechar"]', fecharModal);

    $(document).on('click', '#modal-sugerir-codigo [data-action="confirmar"]', function () {
        if (!alvoAtual) return;
        const codigo = ($('#input-codigo-sugerido').val() || '').trim();
        ajax({
            sinalGxData: 'sugerir',
            missao_origem_id: alvoAtual.missaoId,
            produto_id: alvoAtual.produtoId,
            codigo_sugerido: codigo
        }).then(r => {
            if (!r.ok) { mostrarErroModal(r.erro); return; }
            sugestoesByProduto[alvoAtual.produtoId] = {
                codigo: r.sugestao.codigo_sugerido,
                missaoId: alvoAtual.missaoId
            };
            aplicarChipSugerido(alvoAtual.produtoId, r.sugestao.codigo_sugerido);
            fecharModal();
        }).fail(() => mostrarErroModal('Erro de rede. Tente novamente.'));
    });

    $(document).on('click', '.js-cancelar-sugestao', function () {
        const $c = $(this).closest('.sugestao-codigo-barras');
        const missaoId  = +$c.data('missao-origem');
        const produtoId = +$c.data('produto-id');
        const conf = (window.show && window.show('sugestao.confirma_cancelar')) || 'Cancelar essa sugestão?';
        if (!confirm(conf)) return;
        ajax({ sinalGxData: 'cancelar', missao_origem_id: missaoId, produto_id: produtoId })
            .then(r => {
                if (!r.ok) { alert(r.erro); return; }
                delete sugestoesByProduto[produtoId];
                aplicarBotaoSugerir(produtoId);
            });
    });

    // ESC fecha modal
    $(document).on('keydown', function (e) {
        if (e.key === 'Escape' && $('#modal-sugerir-codigo').is(':visible')) fecharModal();
    });

    // === API pública ===

    function hidratar(missaoId) {
        if (!missaoId) return;
        return ajax({ sinalGxData: 'listar', missao_origem_id: missaoId })
            .then(r => {
                if (!r.ok) return;
                (r.sugestoes || []).forEach(s => {
                    sugestoesByProduto[s.produto_id] = { codigo: s.codigo_sugerido, missaoId: s.missao_origem_id };
                    aplicarChipSugerido(s.produto_id, s.codigo_sugerido);
                });
            });
    }

    window.SugestaoCodigoBarras = { hidratar };
})();
```

- [ ] **Step 2: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/App/html/shared/sugestao_codigo_barras.js
git commit -m "feat(sugestoes-codigo-barras): bundle JS compartilhado"
```

---

### Task 15: i18n pt-br

**Files:**
- Modify: `linguagens/pt-br.json`

- [ ] **Step 1: Ler estrutura atual do pt-br.json e adicionar a chave `sugestao`**

```bash
grep -n "comum\|^}" linguagens/pt-br.json | head -20
```

- [ ] **Step 2: Inserir chaves**

Adicionar (preservando estrutura JSON existente — geralmente nível-1 com objetos por módulo):

```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"
    }
  }
}
```

> Se a chave `comum.cancelar` e `comum.confirmar` não existirem, adicionar também:
> ```json
> "comum": { "cancelar": "Cancelar", "confirmar": "Confirmar" }
> ```

- [ ] **Step 3: Validar JSON**

```bash
python3 -c "import json; json.load(open('linguagens/pt-br.json'))" && echo "JSON OK"
```

Expected: `JSON OK`.

- [ ] **Step 4: Commit**

```bash
git add linguagens/pt-br.json
git commit -m "i18n(pt-br): chaves sugestao.* + comum.cancelar/confirmar"
```

---

### Task 16: Integração na Separação

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/App/html/separacao/item.php`
- Modify: `componentes/Missoes/Logistica/sinais/App/html/separacao/item.js`

- [ ] **Step 1: Localizar onde renderizar a partial na separação**

```bash
grep -n "produto_nome\|descricao\|<?= \$item->descricao\|item-card" componentes/Missoes/Logistica/sinais/App/html/separacao/item.php | head -20
```

Identificar bloco do card de item (provavelmente dentro de loop sobre `$itens`).

- [ ] **Step 2: Incluir partial dentro do card**

No `item.php`, dentro do template do card de produto (após o bloco visível de descrição/quantidade), inserir:

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

E no final do arquivo (uma vez, fora do loop), incluir a modal + carregar o JS:

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

> Se `base_url()` não estiver disponível nesse contexto, usar caminho absoluto: `/componentes/...`. Verificar nos outros scripts incluídos no arquivo o padrão usado.

- [ ] **Step 3: Hidratar no init do `item.js` da separação**

No final do init existente (procurar pelo `$(document).ready` ou função análoga):

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

> Se `window.MISSAO_ID` não estiver setado, identificar como o item.js atual recupera o id da missão (provavelmente via `data-missao-id` em algum container) e passar isso.

- [ ] **Step 4: Verificar no browser**

Abrir a tela de separação com um produto sem `codigo_barras`. Confirmar:
- Botão "Sugerir código" aparece no card.
- Clicar → abre modal.
- Confirmar com código curto → erro inline.
- Confirmar com código válido → chip "Sugerido: ..." substitui o botão.
- Recarregar a página → chip continua aparecendo (hidratação funcionou).
- Botão X → confirm → volta pro botão "Sugerir código".

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/App/html/separacao
git commit -m "feat(sugestoes-codigo-barras): integração na separação (partial + hidratação)"
```

---

### Task 17: Integração no Carregamento

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/App/html/carregamento/item.php`
- Modify: `componentes/Missoes/Logistica/sinais/App/html/carregamento/item.js`

- [ ] **Step 1: Repetir o padrão da Task 16, agora em carregamento/item.php e item.js**

Mesma lógica:
- `include __DIR__ . '/../shared/sugestao_codigo_barras.php';` dentro do loop de itens
- `include __DIR__ . '/../shared/sugestao_codigo_barras_modal.php';` + `<script src=...>` fora do loop
- `SugestaoCodigoBarras.hidratar(window.MISSAO_ID)` no init do item.js

- [ ] **Step 2: Verificar no browser** (mesmos asserts da Task 16, mas pra carregamento)

- [ ] **Step 3: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/App/html/carregamento
git commit -m "feat(sugestoes-codigo-barras): integração no carregamento"
```

---

## Phase 7 — Verificação manual ponta-a-ponta

### Task 18: Smoke test E2E em `dev_gx_goldie`

- [ ] **Step 1: Cenário "com sugestões"**

1. Login como operador no `dev-galpaodasembalagens.goldieapp.com.br`.
2. Iniciar uma SEPARACAO que contenha produto sem `codigo_barras`.
3. Clicar "Sugerir código" → digitar EAN real (ex: 7891234567890) → Confirmar.
4. Chip "Sugerido: ..." aparece. Recarregar a tela — chip persiste.
5. Finalizar a separação normalmente.
6. Validar no DB (`dev_gx_goldie`):
   ```sql
   SELECT id, descricao, status FROM missao_logistica
   WHERE referencia_id = <id_separacao>
     AND descricao LIKE 'Sugestão -%';
   ```
   Expected: 1 linha, status='aguardando_aprovacao'.
7. Como gestor, abrir painel de AJUSTE_DADOS → ver missão "Sugestão - Separação #<id>".
8. Aprovar a missão.
9. Verificar `produtos.codigo_barras` atualizado no Genesis E no ERP (`1goldie_produtos.codigo_barras` no banco do ERP).

- [ ] **Step 2: Cenário "sem sugestões"**

1. Iniciar outra SEPARACAO, **sem** sugerir nada.
2. Finalizar.
3. Validar no DB:
   ```sql
   SELECT COUNT(*) FROM missao_logistica
   WHERE referencia_id = <id_separacao_2>
     AND descricao LIKE 'Sugestão -%';
   ```
   Expected: 0.

- [ ] **Step 3: Cenário "edit + cancel"**

1. SEPARACAO com 1 produto sem `codigo_barras`.
2. Sugerir código A → confirmar.
3. Clicar editar → trocar pra código B → confirmar.
4. Validar no DB: linha em `missao_codigo_sugestao` tem `codigo_sugerido='B'` e status='pendente' (upsert).
5. Clicar X (cancelar).
6. Validar no DB: linha agora tem status='cancelada'.
7. Finalizar separação.
8. Validar no DB: **nenhuma** missão AJUSTE criada (sugestão estava cancelada).

- [ ] **Step 4: Cenário "duplicata"**

1. SEPARACAO com 2 produtos sem `codigo_barras` (A e B).
2. Sugerir código X pro produto A.
3. Tentar sugerir o mesmo código X pro produto B → deve dar erro "Este código já está em outra sugestão pendente".
4. Mudar pra código Y → ok.

- [ ] **Step 5: Cenário "carregamento"**

Repetir cenário 1 mas em CARREGAMENTO. Validar descrição = "Sugestão - Carregamento #<id>".

- [ ] **Step 6: Documentar resultado**

Adicionar nota no PR description (ou em comentário) com:
- IDs das missões de teste
- Print da tela do gestor com a missão "Sugestão -..."
- Confirmação do webhook ERP (produto sincronizado)

---

## Self-Review Pós-Plano

### Spec coverage

- ✅ Sec 5 (estrutura sinal) → Tasks 1-3, 4-9, 10-11
- ✅ Sec 6 (schema) → Task 1
- ✅ Sec 7 (Sonda) → Task 3
- ✅ Sec 8 (validators) → Tasks 4-5
- ✅ Sec 9 (endpoints/dispatcher) → Task 9
- ✅ Sec 10 (laboratórios) → Tasks 6-8, 11
- ✅ Sec 11 (adapter) → Task 10
- ✅ Sec 12 (hook do core) → Task 12
- ✅ Sec 13 (aprovação) → smoke test Task 18
- ✅ Sec 14 (frontend) → Tasks 13-14, 16-17
- ✅ Sec 15 (i18n pt-br) → Task 15
- ✅ Sec 16 (testes) → cobertos nos tasks correspondentes
- ✅ Sec 17 (riscos) → guard idempotência implementado na Task 11

### Pontos de risco já mitigados ou flagados

- **Risco #1 (campo codigo_barras no dados_adicionais)**: Task 10 step 1 instrui verificar o aprovador antes de implementar; payload grava `codigo_barras` E `codigo_barras_sugerido` por redundância.
- **Risco #2 (autoload PSR-4 enxergar o sinal)**: estrutura segue padrão Edicaorota, mesma raiz, mesma convenção de namespace.
- **Risco #6 (re-finalização)**: guard `jaTemAjusteParaOrigem` implementado na Sonda + chamado no `GerarAjusteDeSugestoesLaboratorio`.

### Itens a validar manualmente durante execução

1. **Bootstrap de testes integration** — confirmar se `system/_bootstrap_test.php` existe ou copiar padrão de `Edicaorota/Testes/Laboratorios/CriarEdicaoRotaLaboratorioIntegrationTest.php`. Se inexistente, criar.
2. **`MissaoLogisticaSonda` (V2) vs `MissaoLogistica` (V1)** — usado em validators (Task 4 step 3). Verificar antes; falhar pra fora se ambos inexistirem (usar Connect direto pra leitura como fallback).
3. **API de `MissoesPersistencia::criar()` e `::batchInsertItens()`** — assinatura exata (Task 10 step 4). Ler `MissoesPersistencia.php` linhas 220-410 antes de implementar.
4. **IDs reais em dev pra fixtures de teste** — Tasks 4, 6, 7, 11 assumem IDs `1532` (SEPARACAO ativa), `1533` (CARREGAMENTO ativa), `12345`/`12346` (produtos sem `codigo_barras`). Ajustar pra IDs reais no momento da execução.
5. **`window.MISSAO_ID` nos JS de separação/carregamento** — Tasks 16-17. Pode ser que use outro nome (ex: `MISSAO_LOGISTICA_ID`). Verificar.
6. **`base_url()` helper na view** — Task 16. Pode ser que use `<?= $base_url ?>` ou caminho absoluto.

---

## Execução

Plan complete and saved to `docs/superpowers/plans/2026-05-26-sugestao-codigo-barras-separacao-carregamento.md`.
