# Aprovação por movimento nas Operações WMS — 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:** Dar ao gestor um gate de aprovação **movimento a movimento** sobre as Operações WMS: quando a missão exige aprovação, os movimentos do operador ficam retidos até o gestor confirmar/descartar cada um e enviar.

**Architecture:** Coluna nova `aprovacao_gestor` em `wms_movimentos` (default `aprovado`). Estados: `pendente` (retido) / `rejeitado` (descartar no envio) / `aprovado` (liberado p/ integração). O gate de integração ganha `AND aprovacao_gestor='aprovado'`. O operador grava `pendente` quando a missão exige aprovação; o gestor revisa numa modal (Lab via `labs/apis`) e o "Enviar" comita (pendente→aprovado, rejeitado→cancelada) e conclui a missão pela via canônica (`MissoesAprovacao`). A ativação (deixar o checkbox valer pra WMS) é o **último** passo, mantendo o sistema coerente em cada fase.

**Tech Stack:** PHP (Galaxia V2 / `ModelInteligencia` Sondas), PostgreSQL (`gx_goldie`/`dev_gx_goldie`), MySQL legado não envolvido. Frontend Metronic + JS vanilla + `fetch`. i18n `show()`.

**Spec:** `PRD/operacao-wms/14-aprovacao-movimento-wms-spec.md`

## Global Constraints

- **Bancos DEV apenas:** Postgres `dev_gx_goldie` (host do Connect). Nunca produção.
- **Sem raw SQL no domínio:** escritas via `update()`/`create()` da Sonda; raw SQL só em testes (fixtures) e na query de descoberta de tenants do cron (já é a única exceção de leitura permitida lá).
- **Commits:** NÃO commitar sem o usuário pedir explicitamente (regra do projeto). Os passos "Commit" existem para granularidade; execute-os só com aprovação. Caso contrário, apenas `git add` o stage e siga.
- **i18n:** toda string visível via `show('texto PT literal')`.
- **Sondas V2** (`feedback_sondas_v2`); colunas `block_*` sem DEFAULT (`feedback_block_columns`).
- **Testes de integração:** `dev_gx_goldie`, tenant `block=22`, sessão sintética como em `WmsMovimentoSondaTddTest.php`; sempre limpar resíduo no `finally`.
- **Enum default não-quebrante:** `AprovacaoGestor::APROVADO` é o default em todos os pontos novos — missões sem aprovação e linhas antigas seguem empurrando.
- **Não-regressão:** estas suítes devem continuar verdes a cada passo: `Testes/Sondas/WmsMovimentoSondaTddTest.php`, `Testes/Laboratorios/ConfirmarAlocacaoLaboratorioTddTest.php`, `Testes/Strategies/Operacao/AlocacaoAdapterTddTest.php`, `Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php`.

---

### Task 1: Migration + enum `AprovacaoGestor`

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Migrations/005_add_aprovacao_gestor.sql`
- Create: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Enums/AprovacaoGestor.php`
- Create (test): `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Enums/AprovacaoGestorTddTest.php`

**Interfaces:**
- Produces: enum `GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\AprovacaoGestor` com casos `APROVADO='aprovado'`, `PENDENTE='pendente'`, `REJEITADO='rejeitado'` e `label(): string`.
- Produces: coluna `wms_movimentos.aprovacao_gestor VARCHAR(20) NOT NULL DEFAULT 'aprovado'`.

- [ ] **Step 1: Escrever a migration**

Create `componentes/Missoes/Logistica/sinais/AlocacaoWms/Migrations/005_add_aprovacao_gestor.sql`:

```sql
-- =====================================================
-- Migration 005: gate de aprovação do gestor por movimento WMS
-- Feature: operacao-wms / aprovacao-movimento
-- Data: 2026-06-25
-- =====================================================
-- aprovado  = liberado p/ integração (DEFAULT: linha antiga / missão sem aprovação)
-- pendente  = retido, aguardando o gestor
-- rejeitado = gestor desligou; vira status=cancelada no "Enviar"
-- Idempotente (IF NOT EXISTS): rodar 2x é seguro. Postgres.
-- =====================================================

ALTER TABLE wms_movimentos
    ADD COLUMN IF NOT EXISTS aprovacao_gestor VARCHAR(20) NOT NULL DEFAULT 'aprovado';
```

- [ ] **Step 2: Escrever o enum**

Create `componentes/Missoes/Logistica/sinais/AlocacaoWms/Enums/AprovacaoGestor.php` (espelha `StatusAlocacao.php`):

```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums;

/**
 * AprovacaoGestor — gate de aprovação do gestor por movimento WMS.
 *
 * aprovado  : liberado para integração (default — linha antiga / missão sem aprovação).
 * pendente  : retido, aguardando revisão do gestor (switch ON na revisão).
 * rejeitado : gestor desligou o switch; vira status=cancelada no "Enviar".
 */
enum AprovacaoGestor: string
{
    case APROVADO  = 'aprovado';
    case PENDENTE  = 'pendente';
    case REJEITADO = 'rejeitado';

    public function label(): string
    {
        return match ($this) {
            self::APROVADO  => 'Aprovado',
            self::PENDENTE  => 'Pendente',
            self::REJEITADO => 'Rejeitado',
        };
    }
}
```

- [ ] **Step 3: Escrever o teste do enum (falhando)**

Create `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Enums/AprovacaoGestorTddTest.php`:

```php
<?php
if (php_sapi_name() !== 'cli') { die("Apenas via CLI\n"); }
$_SERVER['HTTP_HOST'] = 'dev-genesis.goldieapp.com.br';
require_once __DIR__ . '/../../../../../../../autoload.php';

use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\AprovacaoGestor;

$passou = 0; $falhas = 0;
function check(bool $c, string $m): void {
    global $passou, $falhas;
    if ($c) { $passou++; echo "[PASS] {$m}\n"; } else { $falhas++; echo "[FAIL] {$m}\n"; }
}

check(AprovacaoGestor::APROVADO->value  === 'aprovado',  'APROVADO = aprovado');
check(AprovacaoGestor::PENDENTE->value  === 'pendente',  'PENDENTE = pendente');
check(AprovacaoGestor::REJEITADO->value === 'rejeitado', 'REJEITADO = rejeitado');
check(AprovacaoGestor::tryFrom('aprovado') === AprovacaoGestor::APROVADO, 'tryFrom aprovado');
check(AprovacaoGestor::PENDENTE->label() === 'Pendente', 'label Pendente');

echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
if ($falhas > 0) { exit(1); }
echo "TODOS OS TESTES PASSARAM\n";
```

- [ ] **Step 4: Rodar o teste do enum**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Enums/AprovacaoGestorTddTest.php`
Expected: PASS=5 FAIL=0 (o enum já existe do Step 2).

- [ ] **Step 5: Aplicar a migration em dev_gx_goldie e verificar a coluna**

Run (script CLI inline, mesmo bootstrap dos testes):

```bash
php -r '
$_SERVER["HTTP_HOST"]="dev-genesis.goldieapp.com.br"; $_SERVER["REQUEST_METHOD"]="GET";
require "autoload.php";
$pdo=(new Galaxia\_Controladores\Connect)->getInstance(null,null,"dev_gx_goldie");
if ($pdo->query("SELECT current_database()")->fetchColumn()!=="dev_gx_goldie"){fwrite(STDERR,"ABORT db\n");exit(1);}
$pdo->exec(file_get_contents("componentes/Missoes/Logistica/sinais/AlocacaoWms/Migrations/005_add_aprovacao_gestor.sql"));
$col=$pdo->query("SELECT column_default, is_nullable, data_type FROM information_schema.columns WHERE table_name=\x27wms_movimentos\x27 AND column_name=\x27aprovacao_gestor\x27")->fetch(PDO::FETCH_ASSOC);
echo json_encode($col).PHP_EOL;
'
```
Expected: imprime algo como `{"column_default":"'aprovado'::character varying","is_nullable":"NO","data_type":"character varying"}`.

- [ ] **Step 6: Commit** (só com aprovação — ver Global Constraints)

```bash
git add componentes/Missoes/Logistica/sinais/AlocacaoWms/Migrations/005_add_aprovacao_gestor.sql \
        componentes/Missoes/Logistica/sinais/AlocacaoWms/Enums/AprovacaoGestor.php \
        componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Enums/AprovacaoGestorTddTest.php
git commit -m "feat(operacao-wms): coluna aprovacao_gestor + enum (gate de aprovação por movimento)"
```

---

### Task 2: Sonda — param de aprovação em `registrarMovimento` + gate de integração + cron

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Sondas/WmsMovimentoSonda.php` (`registrarMovimento` ~111-153; `buscarPendentesIntegracaoErp` ~399-413; `buscarPendentesIntegracaoWms` ~420-433)
- Modify: `crons/wms_alocacao_integracao.php` (query `$sqlBlocks` ~60-68)
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaAprovacaoTddTest.php` (Create)

**Interfaces:**
- Consumes: `AprovacaoGestor` (Task 1).
- Produces: `registrarMovimento(..., ?string $localizacaoDestino = null, AprovacaoGestor $aprovacao = AprovacaoGestor::APROVADO): ?object` — grava `aprovacao_gestor`.
- Produces: `buscarPendentesIntegracao{Erp,Wms}` passam a exigir `aprovacao_gestor='aprovado'`.

- [ ] **Step 1: Escrever o teste (falhando)**

Create `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaAprovacaoTddTest.php`:

```php
<?php
if (php_sapi_name() !== 'cli') { die("Apenas via CLI\n"); }
$_SERVER['HTTP_HOST'] = 'dev-genesis.goldieapp.com.br';
$_SERVER['REQUEST_METHOD'] = 'GET';
require_once __DIR__ . '/../../../../../../../autoload.php';
if (session_status() === PHP_SESSION_NONE) { @session_start(); }

$info = new stdClass();
$info->userData       = (object)['id' => 1, 'username' => 'integ_test', 'name' => 'Integration Test'];
$info->customerData   = (object)['id' => 22];
$info->principalEmpresa= (object)['empresa_id' => 3];
$info->sectorData     = (object)['id' => 1, 'inteligencia' => 'dev_gx_goldie'];
$_SESSION['galaxiaUserInfo'] = $info;
$_SESSION['dashboard']['galaxiaUserInfo'] = $info;

use Galaxia\_Controladores\Connect;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Sondas\WmsMovimentoSonda;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\AprovacaoGestor;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\OperacaoTipo;

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') {
    fwrite(STDERR, "ABORT: não conectou em dev_gx_goldie\n"); exit(1);
}

$passou = 0; $falhas = 0;
function check(bool $c, string $m): void {
    global $passou, $falhas;
    if ($c) { $passou++; echo "[PASS] {$m}\n"; } else { $falhas++; echo "[FAIL] {$m}\n"; }
}

$MISSAO_ID=2723; $PRODUTO_ID=15424; $ITEM_ID=128483; $OPERADOR_ID=1; $BLOCK=22;
$COD='__test_loc_aprov';
$pdo->exec("DELETE FROM wms_localizacoes WHERE codigo='{$COD}'");
$pdo->exec("INSERT INTO wms_localizacoes (codigo, descricao, ativo, block) VALUES ('{$COD}','loc aprov teste',1,{$BLOCK})");
$LOC_ID=(int)$pdo->query("SELECT id FROM wms_localizacoes WHERE codigo='{$COD}'")->fetchColumn();
$pdo->exec("DELETE FROM wms_movimentos WHERE missao_logistica_id={$MISSAO_ID} AND localizacao_codigo='{$COD}'");

$sonda = new WmsMovimentoSonda();
$ID_APROV=null; $ID_PEND=null;

try {
    // 1. default = aprovado → entra na fila de integração
    $movA = $sonda->registrarMovimento($MISSAO_ID,$ITEM_ID,$PRODUTO_ID,$COD,$LOC_ID,1.0,'manual',$OPERADOR_ID,OperacaoTipo::ALOCACAO);
    $ID_APROV=(int)($movA->id ?? 0);
    $colA=$pdo->query("SELECT aprovacao_gestor FROM wms_movimentos WHERE id={$ID_APROV}")->fetchColumn();
    check($colA==='aprovado', 'registrarMovimento default grava aprovado');

    // 2. pendente → retido (fora da fila)
    $movP = $sonda->registrarMovimento($MISSAO_ID,$ITEM_ID,$PRODUTO_ID,$COD,$LOC_ID,1.0,'manual',$OPERADOR_ID,OperacaoTipo::ALOCACAO,null,null,null,AprovacaoGestor::PENDENTE);
    $ID_PEND=(int)($movP->id ?? 0);
    $colP=$pdo->query("SELECT aprovacao_gestor FROM wms_movimentos WHERE id={$ID_PEND}")->fetchColumn();
    check($colP==='pendente', 'registrarMovimento(PENDENTE) grava pendente');

    // 3. gate ERP: inclui aprovado, exclui pendente
    $erp=array_map(fn($r)=>(int)$r->id, $sonda->buscarPendentesIntegracaoErp(500));
    check(in_array($ID_APROV,$erp,true),  'buscarPendentesErp inclui aprovado');
    check(!in_array($ID_PEND,$erp,true),  'buscarPendentesErp exclui pendente');

    // 4. gate WMS: idem
    $wms=array_map(fn($r)=>(int)$r->id, $sonda->buscarPendentesIntegracaoWms(500));
    check(in_array($ID_APROV,$wms,true),  'buscarPendentesWms inclui aprovado');
    check(!in_array($ID_PEND,$wms,true),  'buscarPendentesWms exclui pendente');
} finally {
    $pdo->exec("DELETE FROM wms_movimentos WHERE missao_logistica_id={$MISSAO_ID} AND localizacao_codigo='{$COD}'");
    $pdo->exec("DELETE FROM wms_localizacoes WHERE codigo='{$COD}'");
}

echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
if ($falhas > 0) { exit(1); }
echo "TODOS OS TESTES PASSARAM\n";
```

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

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaAprovacaoTddTest.php`
Expected: FAIL — `registrarMovimento` ainda não aceita o param `AprovacaoGestor` (TypeError) e/ou não grava a coluna.

- [ ] **Step 3: Adicionar o import e o param em `registrarMovimento`**

Em `WmsMovimentoSonda.php`, adicionar o `use` junto aos outros (após linha 9):

```php
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\AprovacaoGestor;
```

Alterar a assinatura de `registrarMovimento` (linha ~120-124) para acrescentar o último parâmetro:

```php
        OperacaoTipo $operacao = OperacaoTipo::ALOCACAO,
        ?string $ocupacaoEstado = null,
        ?string $localizacaoOrigem = null,   // só movimentacao
        ?string $localizacaoDestino = null,  // só movimentacao
        AprovacaoGestor $aprovacao = AprovacaoGestor::APROVADO
    ): ?object {
```

E gravar a coluna no `$this->data` (dentro do array, junto aos demais campos, após `'status' => ...`):

```php
            'status'                     => StatusAlocacao::CONFIRMADA->value,
            'aprovacao_gestor'           => $aprovacao->value,
```

- [ ] **Step 4: Adicionar o gate em `buscarPendentesIntegracaoErp` e `buscarPendentesIntegracaoWms`**

Em `buscarPendentesIntegracaoErp` (~407-410), trocar o `find` por:

```php
        $rows = $this->find(
            "status = '{$confirmada}' AND aprovacao_gestor = 'aprovado' AND integracao_erp IN ('{$pendente}', '{$falha}')",
            ''
        )->order('confirmada_em ASC')->fetch(true);
```

Em `buscarPendentesIntegracaoWms` (~427-430), trocar o `find` por:

```php
        $rows = $this->find(
            "status = '{$confirmada}' AND aprovacao_gestor = 'aprovado' AND integracao_wms IN ('{$pendente}', '{$falha}')",
            ''
        )->order('confirmada_em ASC')->fetch(true);
```

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

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaAprovacaoTddTest.php`
Expected: PASS=6 FAIL=0.

- [ ] **Step 6: Estreitar a query de descoberta de tenants do cron**

Em `crons/wms_alocacao_integracao.php`, na string `$sqlBlocks` (~60-68), adicionar a condição:

```php
$sqlBlocks = "
    SELECT DISTINCT block
    FROM wms_movimentos
    WHERE block IS NOT NULL
      AND block > 0
      AND status = 'confirmada'
      AND aprovacao_gestor = 'aprovado'
      AND (integracao_erp IN ('pendente','falha') OR integracao_wms IN ('pendente','falha'))
    ORDER BY block
";
```

- [ ] **Step 7: Regressão da Sonda + verificação do cron**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaTddTest.php`
Expected: PASS (baseline da Sonda, sem regressão — default aprovado mantém tudo na fila).
Run: `php -l crons/wms_alocacao_integracao.php`
Expected: `No syntax errors detected`.

- [ ] **Step 8: Commit** (só com aprovação)

```bash
git add componentes/Missoes/Logistica/sinais/AlocacaoWms/Sondas/WmsMovimentoSonda.php \
        crons/wms_alocacao_integracao.php \
        componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaAprovacaoTddTest.php
git commit -m "feat(operacao-wms): gate de integração por aprovacao_gestor (Sonda + cron)"
```

---

### Task 3: Sonda — métodos do gestor (listar / alternar / commit)

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Sondas/WmsMovimentoSonda.php` (adicionar 3 métodos ao final da classe, antes do `}` de fechamento)
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaRevisaoTddTest.php` (Create)

**Interfaces:**
- Produces: `listarParaRevisao(int $missaoId): array` — movimentos `status='confirmada'` e `aprovacao_gestor IN ('pendente','rejeitado')` da missão, DESC `confirmada_em`.
- Produces: `alternarAprovacao(int $movimentoId, bool $ligado): int` — `pendente`(ligado)/`rejeitado`(desligado); só age sobre `aprovacao_gestor IN ('pendente','rejeitado')`. Retorna rowCount.
- Produces: `commitAprovacao(int $missaoId): array` — `['aprovados'=>int,'rejeitados'=>int]`; `pendente→aprovado`, `rejeitado→status=cancelada`.

- [ ] **Step 1: Escrever o teste (falhando)**

Create `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaRevisaoTddTest.php`:

```php
<?php
if (php_sapi_name() !== 'cli') { die("Apenas via CLI\n"); }
$_SERVER['HTTP_HOST'] = 'dev-genesis.goldieapp.com.br';
$_SERVER['REQUEST_METHOD'] = 'GET';
require_once __DIR__ . '/../../../../../../../autoload.php';
if (session_status() === PHP_SESSION_NONE) { @session_start(); }

$info = new stdClass();
$info->userData       = (object)['id' => 1, 'username' => 'integ_test', 'name' => 'Integration Test'];
$info->customerData   = (object)['id' => 22];
$info->principalEmpresa= (object)['empresa_id' => 3];
$info->sectorData     = (object)['id' => 1, 'inteligencia' => 'dev_gx_goldie'];
$_SESSION['galaxiaUserInfo'] = $info;
$_SESSION['dashboard']['galaxiaUserInfo'] = $info;

use Galaxia\_Controladores\Connect;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Sondas\WmsMovimentoSonda;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\AprovacaoGestor;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\OperacaoTipo;

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') {
    fwrite(STDERR, "ABORT db\n"); exit(1);
}
$passou=0; $falhas=0;
function check(bool $c, string $m): void { global $passou,$falhas; if($c){$passou++;echo "[PASS] {$m}\n";}else{$falhas++;echo "[FAIL] {$m}\n";} }

$MISSAO_ID=2723; $PRODUTO_ID=15424; $ITEM_ID=128483; $OPERADOR_ID=1; $BLOCK=22;
$COD='__test_loc_revisao';
$pdo->exec("DELETE FROM wms_localizacoes WHERE codigo='{$COD}'");
$pdo->exec("INSERT INTO wms_localizacoes (codigo, descricao, ativo, block) VALUES ('{$COD}','loc revisao',1,{$BLOCK})");
$LOC_ID=(int)$pdo->query("SELECT id FROM wms_localizacoes WHERE codigo='{$COD}'")->fetchColumn();
$pdo->exec("DELETE FROM wms_movimentos WHERE missao_logistica_id={$MISSAO_ID} AND localizacao_codigo='{$COD}'");

$sonda = new WmsMovimentoSonda();
try {
    // 2 movimentos pendentes
    $a=$sonda->registrarMovimento($MISSAO_ID,$ITEM_ID,$PRODUTO_ID,$COD,$LOC_ID,1.0,'manual',$OPERADOR_ID,OperacaoTipo::ALOCACAO,null,null,null,AprovacaoGestor::PENDENTE);
    $b=$sonda->registrarMovimento($MISSAO_ID,$ITEM_ID,$PRODUTO_ID,$COD,$LOC_ID,2.0,'manual',$OPERADOR_ID,OperacaoTipo::ALOCACAO,null,null,null,AprovacaoGestor::PENDENTE);
    $IDA=(int)$a->id; $IDB=(int)$b->id;

    // listarParaRevisao traz os dois
    $rev=array_map(fn($r)=>(int)$r->id, $sonda->listarParaRevisao($MISSAO_ID));
    check(in_array($IDA,$rev,true) && in_array($IDB,$rev,true), 'listarParaRevisao traz os pendentes');

    // alternar B para rejeitado
    $n=$sonda->alternarAprovacao($IDB,false);
    check($n===1, 'alternarAprovacao retorna 1 linha');
    $colB=$pdo->query("SELECT aprovacao_gestor FROM wms_movimentos WHERE id={$IDB}")->fetchColumn();
    check($colB==='rejeitado', 'B virou rejeitado');

    // commit: A pendente→aprovado, B rejeitado→cancelada
    $res=$sonda->commitAprovacao($MISSAO_ID);
    check(($res['aprovados']??0)>=1, 'commit aprovados >= 1');
    check(($res['rejeitados']??0)>=1, 'commit rejeitados >= 1');
    $rowA=$pdo->query("SELECT aprovacao_gestor,status FROM wms_movimentos WHERE id={$IDA}")->fetch(PDO::FETCH_OBJ);
    $rowB=$pdo->query("SELECT aprovacao_gestor,status FROM wms_movimentos WHERE id={$IDB}")->fetch(PDO::FETCH_OBJ);
    check($rowA->aprovacao_gestor==='aprovado' && $rowA->status==='confirmada', 'A aprovado+confirmada (vai pro cron)');
    check($rowB->status==='cancelada', 'B cancelada (descartado)');

    // após commit, listarParaRevisao não traz mais nada da missão
    $rev2=array_map(fn($r)=>(int)$r->id, $sonda->listarParaRevisao($MISSAO_ID));
    check(!in_array($IDA,$rev2,true) && !in_array($IDB,$rev2,true), 'listarParaRevisao vazio após commit');
} finally {
    $pdo->exec("DELETE FROM wms_movimentos WHERE missao_logistica_id={$MISSAO_ID} AND localizacao_codigo='{$COD}'");
    $pdo->exec("DELETE FROM wms_localizacoes WHERE codigo='{$COD}'");
}
echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
if ($falhas > 0) { exit(1); }
echo "TODOS OS TESTES PASSARAM\n";
```

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

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaRevisaoTddTest.php`
Expected: FAIL — métodos `listarParaRevisao`/`alternarAprovacao`/`commitAprovacao` não existem.

- [ ] **Step 3: Implementar os 3 métodos**

Em `WmsMovimentoSonda.php`, adicionar antes do `}` final da classe:

```php
    /**
     * Movimentos da missão aguardando decisão do gestor (revisão por movimento).
     * Apenas confirmados e ainda não comitados (pendente/rejeitado).
     *
     * @return array<int,object>
     */
    public function listarParaRevisao(int $missaoId): array
    {
        $confirmada = StatusAlocacao::CONFIRMADA->value;
        $rows = $this->find(
            "missao_logistica_id = :m AND status = '{$confirmada}' AND aprovacao_gestor IN ('pendente','rejeitado')",
            "m={$missaoId}"
        )->order('confirmada_em DESC')->fetch(true);

        return $rows ?: [];
    }

    /**
     * Alterna a decisão do gestor para UM movimento, sem comitar.
     * ligado = pendente (será enviado); desligado = rejeitado (será descartado).
     * Guard: só age sobre movimentos ainda não comitados (pendente/rejeitado).
     *
     * @return int linhas afetadas (0 se já comitado/inexistente)
     */
    public function alternarAprovacao(int $movimentoId, bool $ligado): int
    {
        $novo = $ligado
            ? AprovacaoGestor::PENDENTE->value
            : AprovacaoGestor::REJEITADO->value;

        return (int)$this->update(
            ['aprovacao_gestor' => $novo],
            "id = :id AND aprovacao_gestor IN ('pendente','rejeitado')",
            "id={$movimentoId}"
        );
    }

    /**
     * Comita a revisão da missão: pendente→aprovado (libera p/ cron),
     * rejeitado→status=cancelada (descarta). Idempotente.
     *
     * @return array{aprovados:int, rejeitados:int}
     */
    public function commitAprovacao(int $missaoId): array
    {
        $confirmada = StatusAlocacao::CONFIRMADA->value;
        $cancelada  = StatusAlocacao::CANCELADA->value;

        $aprovados = (int)$this->update(
            ['aprovacao_gestor' => AprovacaoGestor::APROVADO->value],
            "missao_logistica_id = :m AND status = '{$confirmada}' AND aprovacao_gestor = 'pendente'",
            "m={$missaoId}"
        );

        $rejeitados = (int)$this->update(
            ['status' => $cancelada],
            "missao_logistica_id = :m AND status = '{$confirmada}' AND aprovacao_gestor = 'rejeitado'",
            "m={$missaoId}"
        );

        return ['aprovados' => $aprovados, 'rejeitados' => $rejeitados];
    }
```

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

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaRevisaoTddTest.php`
Expected: PASS=8 FAIL=0.

- [ ] **Step 5: Commit** (só com aprovação)

```bash
git add componentes/Missoes/Logistica/sinais/AlocacaoWms/Sondas/WmsMovimentoSonda.php \
        componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaRevisaoTddTest.php
git commit -m "feat(operacao-wms): métodos de revisão na Sonda (listar/alternar/commit)"
```

---

### Task 4: Carimbar `pendente` na gravação do operador

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/ConfirmarAlocacaoLaboratorio.php` (imports topo; bloco de persistência ~106-134)
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/ConfirmarAlocacaoAprovacaoTddTest.php` (Create)

**Interfaces:**
- Consumes: `WmsMovimentoSonda::registrarMovimento(..., AprovacaoGestor $aprovacao)` (Task 2); `AprovacaoGestor` (Task 1).
- Produces: o movimento nasce `pendente` quando `$missao->requer_aprovacao` é verdadeiro; `aprovado` (default) caso contrário.

> Nota: este passo fica **dormente** até a Task 8 (criação ainda força `requer_aprovacao='0'`), então não muda o comportamento atual. O teste cria a missão diretamente com `requer_aprovacao=1` para exercitar o caminho.

- [ ] **Step 1: Escrever o teste (falhando)**

Create `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/ConfirmarAlocacaoAprovacaoTddTest.php`:

```php
<?php
if (php_sapi_name() !== 'cli') { die("Apenas via CLI\n"); }
$_SERVER['HTTP_HOST'] = 'dev-genesis.goldieapp.com.br';
$_SERVER['REQUEST_METHOD'] = 'GET';
require_once __DIR__ . '/../../../../../../../autoload.php';
if (session_status() === PHP_SESSION_NONE) { @session_start(); }

$info = new stdClass();
$info->userData       = (object)['id' => 1, 'username' => 'integ_test', 'name' => 'Integration Test'];
$info->customerData   = (object)['id' => 22];
$info->principalEmpresa= (object)['empresa_id' => 3];
$info->sectorData     = (object)['id' => 1, 'inteligencia' => 'dev_gx_goldie'];
$_SESSION['galaxiaUserInfo'] = $info;
$_SESSION['dashboard']['galaxiaUserInfo'] = $info;

use Galaxia\_Controladores\Connect;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Laboratorios\ConfirmarAlocacaoLaboratorio;

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') { fwrite(STDERR,"ABORT db\n"); exit(1); }
$passou=0; $falhas=0;
function check(bool $c, string $m): void { global $passou,$falhas; if($c){$passou++;echo "[PASS] {$m}\n";}else{$falhas++;echo "[FAIL] {$m}\n";} }

$PRODUTO_ID=15424; $OPERADOR_ID=1; $BLOCK=22;
$COD='__test_loc_confaprov';
$pdo->exec("DELETE FROM wms_localizacoes WHERE codigo='{$COD}'");
$pdo->exec("INSERT INTO wms_localizacoes (codigo, descricao, ativo, block) VALUES ('{$COD}','loc conf aprov',1,{$BLOCK})");

// Missão WMS com requer_aprovacao=1 (tipo_missao_id=14 = ALOCACAO_WMS)
$pdo->exec("INSERT INTO missao_logistica (tipo_missao_id, status, requer_aprovacao, lider_id, configuracoes, block, data_cadastro)
            VALUES (14,'em_andamento',1,1,'{\"operacao_tipo\":\"alocacao\",\"modo_alocacao\":\"livre\"}',{$BLOCK},NOW())");
$MISSAO_ID=(int)$pdo->query("SELECT id FROM missao_logistica WHERE block={$BLOCK} ORDER BY id DESC LIMIT 1")->fetchColumn();

try {
    $lab = new ConfirmarAlocacaoLaboratorio();
    $r = $lab->executar([
        'missao_id'         => $MISSAO_ID,
        'operador_id'       => $OPERADOR_ID,
        'produto_id'        => $PRODUTO_ID,
        'localizacao_codigo'=> $COD,
        'origem_leitura'    => 'manual',
        'quantidade'        => 3,
    ]);
    check(($r['ok'] ?? false) === true, 'confirmar ok em missão com aprovação');
    $col=$pdo->query("SELECT aprovacao_gestor FROM wms_movimentos WHERE missao_logistica_id={$MISSAO_ID} ORDER BY id DESC LIMIT 1")->fetchColumn();
    check($col==='pendente', 'movimento nasce pendente quando requer_aprovacao=1');
} finally {
    $pdo->exec("DELETE FROM wms_movimentos WHERE missao_logistica_id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM missao_logistica_itens WHERE missao_logistica_id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM wms_localizacoes WHERE codigo='{$COD}'");
}
echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
if ($falhas > 0) { exit(1); }
echo "TODOS OS TESTES PASSARAM\n";
```

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

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/ConfirmarAlocacaoAprovacaoTddTest.php`
Expected: FAIL — `aprovacao_gestor` grava `aprovado` (default), não `pendente`.

- [ ] **Step 3: Implementar — ler `requer_aprovacao` e passar `PENDENTE`**

Em `ConfirmarAlocacaoLaboratorio.php`, adicionar o import (junto aos outros `use`, após a linha 8):

```php
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\AprovacaoGestor;
```

No método `executar`, logo após `$adapter = $validacao['adapter'];` (linha ~78), calcular o estado inicial:

```php
        // Gate de aprovação: missão que exige aprovação retém o movimento (pendente)
        // até o gestor revisar; senão nasce aprovado (default → vai pro cron).
        $requerAprovacao  = isset($missao->requer_aprovacao) ? (bool)(int)$missao->requer_aprovacao : false;
        $aprovacaoInicial = $requerAprovacao ? AprovacaoGestor::PENDENTE : AprovacaoGestor::APROVADO;
```

Na chamada `$this->alocacaoSonda->registrarMovimento(...)` (linha ~121-134), acrescentar o argumento final:

```php
            $mov['localizacao_codigo_origem'] ?? null,
            $mov['localizacao_codigo_destino'] ?? null,
            $aprovacaoInicial
        );
```

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

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/ConfirmarAlocacaoAprovacaoTddTest.php`
Expected: PASS=2 FAIL=0.

- [ ] **Step 5: Regressão do confirmar (sem aprovação = aprovado)**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/ConfirmarAlocacaoLaboratorioTddTest.php`
Expected: PASS (missões sem `requer_aprovacao` seguem gravando `aprovado` por default → cron empurra como hoje).

- [ ] **Step 6: Commit** (só com aprovação)

```bash
git add componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/ConfirmarAlocacaoLaboratorio.php \
        componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/ConfirmarAlocacaoAprovacaoTddTest.php
git commit -m "feat(operacao-wms): operador grava movimento pendente quando missão exige aprovação"
```

---

### Task 5: Backend do "Enviar" — `MissoesAprovacao::aprovarOperacaoWms`

**Files:**
- Modify: `componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesAprovacao.php` (match do construtor ~52-57; adicionar método novo)
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/AprovarOperacaoWmsTddTest.php` (Create)

**Interfaces:**
- Consumes: `WmsMovimentoSonda::commitAprovacao(int): array` (Task 3) via `class_exists` (acoplamento frouxo entre camadas, como o hook de AJUSTE_DADOS já faz).
- Consumes: helpers internos `buscarMissao`, `obterGestorId`, `finalizarMissao`, `registrarExtrato`.
- Produces: opção `aprovarOperacaoWms` no construtor — comita os movimentos e conclui a missão (`CONCLUIDA`).

- [ ] **Step 1: Escrever o teste de integração (falhando)**

Create `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/AprovarOperacaoWmsTddTest.php`:

```php
<?php
if (php_sapi_name() !== 'cli') { die("Apenas via CLI\n"); }
$_SERVER['HTTP_HOST'] = 'dev-genesis.goldieapp.com.br';
$_SERVER['REQUEST_METHOD'] = 'GET';
require_once __DIR__ . '/../../../../../../../autoload.php';
if (session_status() === PHP_SESSION_NONE) { @session_start(); }

$info = new stdClass();
$info->userData       = (object)['id' => 1, 'username' => 'integ_test', 'name' => 'Integration Test', 'gestor_logistico' => 1];
$info->customerData   = (object)['id' => 22];
$info->principalEmpresa= (object)['empresa_id' => 3];
$info->sectorData     = (object)['id' => 1, 'inteligencia' => 'dev_gx_goldie'];
$_SESSION['galaxiaUserInfo'] = $info;
$_SESSION['dashboard']['galaxiaUserInfo'] = $info;

use Galaxia\_Controladores\Connect;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Sondas\WmsMovimentoSonda;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\AprovacaoGestor;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\OperacaoTipo;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Satelites\MissoesAprovacao;

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') { fwrite(STDERR,"ABORT db\n"); exit(1); }
$passou=0; $falhas=0;
function check(bool $c, string $m): void { global $passou,$falhas; if($c){$passou++;echo "[PASS] {$m}\n";}else{$falhas++;echo "[FAIL] {$m}\n";} }

$PRODUTO_ID=15424; $ITEM_ID=128483; $OPERADOR_ID=1; $BLOCK=22;
$COD='__test_loc_envia';
$pdo->exec("DELETE FROM wms_localizacoes WHERE codigo='{$COD}'");
$pdo->exec("INSERT INTO wms_localizacoes (codigo, descricao, ativo, block) VALUES ('{$COD}','loc envia',1,{$BLOCK})");
$LOC_ID=(int)$pdo->query("SELECT id FROM wms_localizacoes WHERE codigo='{$COD}'")->fetchColumn();
$pdo->exec("INSERT INTO missao_logistica (tipo_missao_id, status, requer_aprovacao, lider_id, configuracoes, block, data_cadastro)
            VALUES (14,'aguardando_aprovacao',1,1,'{\"operacao_tipo\":\"alocacao\"}',{$BLOCK},NOW())");
$MISSAO_ID=(int)$pdo->query("SELECT id FROM missao_logistica WHERE block={$BLOCK} ORDER BY id DESC LIMIT 1")->fetchColumn();

$sonda = new WmsMovimentoSonda();
try {
    $a=$sonda->registrarMovimento($MISSAO_ID,$ITEM_ID,$PRODUTO_ID,$COD,$LOC_ID,1.0,'manual',$OPERADOR_ID,OperacaoTipo::ALOCACAO,null,null,null,AprovacaoGestor::PENDENTE);
    $b=$sonda->registrarMovimento($MISSAO_ID,$ITEM_ID,$PRODUTO_ID,$COD,$LOC_ID,2.0,'manual',$OPERADOR_ID,OperacaoTipo::ALOCACAO,null,null,null,AprovacaoGestor::PENDENTE);
    $IDA=(int)$a->id; $IDB=(int)$b->id;
    $sonda->alternarAprovacao($IDB,false); // B descartado

    // galaxia minimal stub: aprovarOperacaoWms usa galaxiaResposta()->removerElemento()->feito()
    $galaxia = new class {
        public function galaxiaResposta($m){ return $this; }
        public function removerElemento($s){ return $this; }
        public function feito(){ return ['ok'=>true]; }
    };

    new MissoesAprovacao($galaxia, 'aprovarOperacaoWms', ['missao_id'=>$MISSAO_ID, 'usuario_id'=>1]);

    $rowA=$pdo->query("SELECT aprovacao_gestor,status FROM wms_movimentos WHERE id={$IDA}")->fetch(PDO::FETCH_OBJ);
    $rowB=$pdo->query("SELECT status FROM wms_movimentos WHERE id={$IDB}")->fetch(PDO::FETCH_OBJ);
    $statusMissao=$pdo->query("SELECT status FROM missao_logistica WHERE id={$MISSAO_ID}")->fetchColumn();

    check($rowA->aprovacao_gestor==='aprovado' && $rowA->status==='confirmada', 'A liberado (aprovado+confirmada)');
    check($rowB->status==='cancelada', 'B descartado (cancelada)');
    check($statusMissao==='concluido' || $statusMissao==='concluida', 'missão concluída');
} finally {
    $pdo->exec("DELETE FROM wms_movimentos WHERE missao_logistica_id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM missao_logistica_itens WHERE missao_logistica_id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM wms_localizacoes WHERE codigo='{$COD}'");
}
echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
if ($falhas > 0) { exit(1); }
echo "TODOS OS TESTES PASSARAM\n";
```

> **Nota de execução:** confirme o valor exato do enum `StatusMissao::CONCLUIDA->value` ao implementar (o teste aceita `concluido`/`concluida`). Use o valor real do enum no código, não uma string literal.

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

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/AprovarOperacaoWmsTddTest.php`
Expected: FAIL — opção `aprovarOperacaoWms` cai no `default` ('Opção inválida'); nada é comitado.

- [ ] **Step 3: Adicionar o arm no match do construtor**

Em `MissoesAprovacao.php`, no `match ($opcao)` (linha ~52-57), adicionar antes do `default`:

```php
            'aprovarOperacaoWms' => $this->aprovarOperacaoWms($data),
```

- [ ] **Step 4: Implementar o método**

Em `MissoesAprovacao.php`, adicionar o método (após `aprovarSimples`, antes de `despacharWebhooksFaltas`):

```php
    /**
     * Aprovação por movimento das Operações WMS.
     *
     * As decisões já foram salvas por movimento (pendente/rejeitado) durante a
     * revisão. Aqui comitamos: pendente→aprovado (libera p/ cron),
     * rejeitado→cancelada (descarta), e concluímos a missão pela via canônica
     * (dispara o hook de AJUSTE_DADOS já existente para ALOCACAO_WMS).
     *
     * Acoplamento frouxo com o sinal AlocacaoWms via class_exists (mesmo padrão
     * do hook de sugestões) — o Satélite não depende do sinal estar deployado.
     */
    private function aprovarOperacaoWms(array $data)
    {
        $missaoId = $data['missao_id'] ?? null;
        $gestorId = $this->obterGestorId($data);

        if (!$missaoId) {
            return (new Message())->error('Missão não informada')->render();
        }

        $missao = $this->buscarMissao($missaoId);
        if (!$missao) {
            return (new Message())->error('Missão não encontrada')->render();
        }
        if ((int)$missao->tipo_missao_id !== TipoMissao::ALOCACAO_WMS->id()) {
            return (new Message())->error('Operação válida apenas para missões WMS')->render();
        }
        if ($missao->status !== StatusMissao::AGUARDANDO_APROVACAO->value) {
            return (new Message())->error('Missão não está aguardando aprovação')->render();
        }

        $resumo = ['aprovados' => 0, 'rejeitados' => 0];
        $cls = '\GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Sondas\WmsMovimentoSonda';
        if (class_exists($cls)) {
            try {
                $sonda  = new $cls();
                $resumo = $sonda->commitAprovacao((int)$missaoId);
            } catch (\Throwable $e) {
                error_log('[aprovarOperacaoWms] commit falhou: ' . $e->getMessage());
            }
        }

        $this->finalizarMissao($missaoId, $gestorId);

        $this->registrarExtrato($missaoId, AcaoExtrato::MISSAO_CONCLUIDA, null, [
            'aprovado_por'   => $gestorId,
            'tipo_aprovacao' => 'operacao_wms',
            'mov_aprovados'  => $resumo['aprovados'],
            'mov_rejeitados' => $resumo['rejeitados'],
        ]);

        $msgData = (new Message())->success(
            "Missão concluída! {$resumo['aprovados']} movimento(s) enviado(s), {$resumo['rejeitados']} descartado(s)."
        )->render();

        return $this->galaxia->galaxiaResposta($msgData)
            ->removerElemento(".mission-row[data-id={$missaoId}]")
            ->feito();
    }
```

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

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/AprovarOperacaoWmsTddTest.php`
Expected: PASS=3 FAIL=0.

- [ ] **Step 6: Commit** (só com aprovação)

```bash
git add componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesAprovacao.php \
        componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/AprovarOperacaoWmsTddTest.php
git commit -m "feat(operacao-wms): MissoesAprovacao::aprovarOperacaoWms (commit + conclusão)"
```

---

### Task 6: Lab `RevisarMovimentosLaboratorio` (listar / alternar) via labs/apis

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/RevisarMovimentosLaboratorio.php`
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/RevisarMovimentosLaboratorioTddTest.php`

**Interfaces:**
- Consumes: `WmsMovimentoSonda::listarParaRevisao` / `alternarAprovacao` (Task 3); `Produto::findById` para o nome.
- Produces: classe com `__construct($galaxia, string $sinal, ?array $data)` (constructor-dispatch, espelha `CuradoriaAlertasTmsLab`); `$this->response` JSON-serializável.
- Rotas: `GET /labs/apis/Logistica/AlocacaoWms/RevisarMovimentosLaboratorio/listar?missao_id=N`; `POST .../alternar { movimento_id, ligado }`.

- [ ] **Step 1: Escrever o teste (falhando)**

Create `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/RevisarMovimentosLaboratorioTddTest.php`:

```php
<?php
if (php_sapi_name() !== 'cli') { die("Apenas via CLI\n"); }
$_SERVER['HTTP_HOST'] = 'dev-genesis.goldieapp.com.br';
$_SERVER['REQUEST_METHOD'] = 'GET';
require_once __DIR__ . '/../../../../../../../autoload.php';
if (session_status() === PHP_SESSION_NONE) { @session_start(); }

$info = new stdClass();
$info->userData       = (object)['id' => 1, 'username' => 'integ_test', 'name' => 'Integration Test', 'gestor_logistico' => 1];
$info->customerData   = (object)['id' => 22];
$info->principalEmpresa= (object)['empresa_id' => 3];
$info->sectorData     = (object)['id' => 1, 'inteligencia' => 'dev_gx_goldie'];
$_SESSION['galaxiaUserInfo'] = $info;
$_SESSION['dashboard']['galaxiaUserInfo'] = $info;

use Galaxia\_Controladores\Connect;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Sondas\WmsMovimentoSonda;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\AprovacaoGestor;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\OperacaoTipo;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Laboratorios\RevisarMovimentosLaboratorio;

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') { fwrite(STDERR,"ABORT db\n"); exit(1); }
$passou=0; $falhas=0;
function check(bool $c, string $m): void { global $passou,$falhas; if($c){$passou++;echo "[PASS] {$m}\n";}else{$falhas++;echo "[FAIL] {$m}\n";} }

$PRODUTO_ID=15424; $ITEM_ID=128483; $OPERADOR_ID=1; $BLOCK=22;
$COD='__test_loc_lab';
$pdo->exec("DELETE FROM wms_localizacoes WHERE codigo='{$COD}'");
$pdo->exec("INSERT INTO wms_localizacoes (codigo, descricao, ativo, block) VALUES ('{$COD}','loc lab',1,{$BLOCK})");
$LOC_ID=(int)$pdo->query("SELECT id FROM wms_localizacoes WHERE codigo='{$COD}'")->fetchColumn();
$pdo->exec("INSERT INTO missao_logistica (tipo_missao_id, status, requer_aprovacao, lider_id, configuracoes, block, data_cadastro)
            VALUES (14,'aguardando_aprovacao',1,1,'{\"operacao_tipo\":\"alocacao\"}',{$BLOCK},NOW())");
$MISSAO_ID=(int)$pdo->query("SELECT id FROM missao_logistica WHERE block={$BLOCK} ORDER BY id DESC LIMIT 1")->fetchColumn();

$sonda = new WmsMovimentoSonda();
try {
    $a=$sonda->registrarMovimento($MISSAO_ID,$ITEM_ID,$PRODUTO_ID,$COD,$LOC_ID,1.0,'manual',$OPERADOR_ID,OperacaoTipo::ALOCACAO,null,null,null,AprovacaoGestor::PENDENTE);
    $IDA=(int)$a->id;

    // listar
    $_GET['missao_id']=$MISSAO_ID;
    $lab=new RevisarMovimentosLaboratorio(null,'listar',['missao_id'=>$MISSAO_ID]);
    $resp=is_string($lab->response)?json_decode($lab->response,true):$lab->response;
    check(!empty($resp['ok']), 'listar ok');
    $ids=array_map(fn($m)=>(int)$m['id'], $resp['movimentos'] ?? []);
    check(in_array($IDA,$ids,true), 'listar traz o movimento pendente');
    $primeiro = ($resp['movimentos'][0] ?? []);
    check(array_key_exists('produto_nome',$primeiro), 'listar enriquece com produto_nome');

    // alternar (desligar)
    $lab2=new RevisarMovimentosLaboratorio(null,'alternar',['movimento_id'=>$IDA,'ligado'=>0]);
    $resp2=is_string($lab2->response)?json_decode($lab2->response,true):$lab2->response;
    check(!empty($resp2['ok']), 'alternar ok');
    $col=$pdo->query("SELECT aprovacao_gestor FROM wms_movimentos WHERE id={$IDA}")->fetchColumn();
    check($col==='rejeitado', 'alternar(0) gravou rejeitado');
} finally {
    $pdo->exec("DELETE FROM wms_movimentos WHERE missao_logistica_id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM missao_logistica_itens WHERE missao_logistica_id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM missao_logistica WHERE id={$MISSAO_ID}");
    $pdo->exec("DELETE FROM wms_localizacoes WHERE codigo='{$COD}'");
}
echo "\nRESULTADO: PASS={$passou} FAIL={$falhas}\n";
if ($falhas > 0) { exit(1); }
echo "TODOS OS TESTES PASSARAM\n";
```

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

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/RevisarMovimentosLaboratorioTddTest.php`
Expected: FAIL — classe `RevisarMovimentosLaboratorio` não existe.

- [ ] **Step 3: Implementar o Lab**

Create `componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/RevisarMovimentosLaboratorio.php`:

```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Laboratorios;

use Base_config\Helpers\Message;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Sondas\WmsMovimentoSonda;
use GalaxiaConstelacoes\Produtos\Crud\Inteligencias\Produto;

/**
 * RevisarMovimentosLaboratorio — backend da revisão por movimento (gestor).
 *
 * Constructor-dispatch (espelha CuradoriaAlertasTmsLab). Escopo por tenant via
 * customerControl da Sonda (sessão do gestor logado).
 *
 * Rotas (labs/apis):
 *   GET  /labs/apis/Logistica/AlocacaoWms/RevisarMovimentosLaboratorio/listar?missao_id=N
 *   POST /labs/apis/Logistica/AlocacaoWms/RevisarMovimentosLaboratorio/alternar  { movimento_id, ligado }
 */
class RevisarMovimentosLaboratorio
{
    public $response;
    public $galaxia;
    public $sinal;

    public function __construct($galaxia, string $sinal, ?array $data = [])
    {
        $this->galaxia = $galaxia;
        $this->sinal   = $sinal;

        switch ($sinal) {
            case 'listar':   $this->response = $this->listar($data ?? []);   break;
            case 'alternar': $this->response = $this->alternar($data ?? []); break;
            default:
                $this->response = json_encode(['ok' => false, 'erro' => 'Opção inválida']);
        }
    }

    private function listar(array $data): string
    {
        $missaoId = (int)($data['missao_id'] ?? $_GET['missao_id'] ?? $_POST['missao_id'] ?? 0);
        if ($missaoId <= 0) {
            return json_encode(['ok' => false, 'erro' => 'Missão não informada']);
        }

        $sonda = new WmsMovimentoSonda();
        $rows  = $sonda->listarParaRevisao($missaoId);

        // cache de nomes de produto (evita N queries repetidas)
        $nomes = [];
        $produtoModel = new Produto();

        $movimentos = [];
        foreach ($rows as $r) {
            $pid = (int)$r->produto_id;
            if (!isset($nomes[$pid])) {
                $prod = $produtoModel->findById($pid);
                if (is_array($prod)) { $prod = $prod[0] ?? null; }
                $nomes[$pid] = [
                    'nome'   => $prod->descricao ?? ('Produto #' . $pid),
                    'codigo' => $prod->codigo_produto ?? '',
                ];
            }
            $movimentos[] = [
                'id'             => (int)$r->id,
                'operacao_tipo'  => $r->operacao_tipo ?? 'alocacao',
                'produto_nome'   => $nomes[$pid]['nome'],
                'produto_codigo' => $nomes[$pid]['codigo'],
                'localizacao'    => $r->localizacao_codigo ?? '',
                'origem'         => $r->localizacao_codigo_origem ?? null,
                'destino'        => $r->localizacao_codigo_destino ?? null,
                'quantidade'     => $r->quantidade,
                'operador_id'    => (int)($r->operador_id ?? 0),
                'confirmada_em'  => $r->confirmada_em ?? null,
                // switch ON = pendente (será enviado); OFF = rejeitado (será descartado)
                'ligado'         => (($r->aprovacao_gestor ?? 'pendente') !== 'rejeitado'),
            ];
        }

        return json_encode(['ok' => true, 'missao_id' => $missaoId, 'movimentos' => $movimentos]);
    }

    private function alternar(array $data): string
    {
        $movimentoId = (int)($data['movimento_id'] ?? $_POST['movimento_id'] ?? 0);
        $ligadoRaw   = $data['ligado'] ?? $_POST['ligado'] ?? null;
        if ($movimentoId <= 0 || $ligadoRaw === null) {
            return json_encode(['ok' => false, 'erro' => 'Parâmetros inválidos']);
        }
        $ligado = filter_var($ligadoRaw, FILTER_VALIDATE_BOOLEAN);

        $sonda = new WmsMovimentoSonda();
        $n = $sonda->alternarAprovacao($movimentoId, $ligado);

        return json_encode(['ok' => $n > 0, 'movimento_id' => $movimentoId, 'ligado' => $ligado]);
    }
}
```

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

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/RevisarMovimentosLaboratorioTddTest.php`
Expected: PASS=5 FAIL=0.

- [ ] **Step 5: Commit** (só com aprovação)

```bash
git add componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/RevisarMovimentosLaboratorio.php \
        componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/RevisarMovimentosLaboratorioTddTest.php
git commit -m "feat(operacao-wms): RevisarMovimentosLaboratorio (listar/alternar via labs/apis)"
```

---

### Task 7: Frontend — modal de revisão + controller + botão na lista

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Designacoes/forms/revisar_movimentos_wms.php` (modal: tabela + JS inline)
- Modify: `componentes/Missoes/Logistica/sinais/Designacoes/Designacoes.php` (novo método `revisarMovimentosWms`)
- Modify: `componentes/Missoes/Logistica/sinais/Designacoes/html/designacoes_lista.php` (branch do botão para ALOCACAO_WMS + AGUARDANDO_APROVACAO, ~linhas 1385-1416)

**Interfaces:**
- Consumes: `RevisarMovimentosLaboratorio` via `/labs/apis/Logistica/AlocacaoWms/RevisarMovimentosLaboratorio/{listar,alternar}` (Task 6); `MissoesAprovacao::aprovarOperacaoWms` via ação-elemento (Task 5).
- Produces: método `Designacoes::revisarMovimentosWms(?array $data)` que renderiza a modal; botão "Revisar movimentos" na lista.

- [ ] **Step 1: Criar a view da modal**

Create `componentes/Missoes/Logistica/sinais/Designacoes/forms/revisar_movimentos_wms.php`:

```php
<?php
/** @var array $data — espera $data['missao_id'] */
$missaoId = (int)($data['missao_id'] ?? 0);
// Ação canônica do "Enviar" (mesmo mecanismo dos botões de aprovação da lista).
$acaoEnviar = $galaxia
    ->setConstelacao('Logistica')->setEstrela('MissaoLogistica')
    ->setSatelite('MissoesAprovacao')->setOpcao('aprovarOperacaoWms')
    ->acaoElemento('missao_id=' . $missaoId);
?>
<div class="rmw-modal" data-missao="<?= $missaoId ?>">
    <div style="display:flex; gap:.5rem; margin-bottom:.75rem;">
        <button type="button" class="btn btn-sm btn-light" id="rmw-all-on"><?= show('Confirmar todos') ?></button>
        <button type="button" class="btn btn-sm btn-light" id="rmw-all-off"><?= show('Descartar todos') ?></button>
        <span class="ms-auto text-muted" id="rmw-count"></span>
    </div>

    <div class="table-responsive" style="max-height:55vh; overflow:auto;">
        <table class="table table-sm align-middle">
            <thead>
                <tr>
                    <th><?= show('Produto') ?></th>
                    <th><?= show('Endereço') ?></th>
                    <th class="text-end"><?= show('Qtd') ?></th>
                    <th><?= show('Quando') ?></th>
                    <th class="text-center"><?= show('Enviar') ?></th>
                </tr>
            </thead>
            <tbody id="rmw-tbody">
                <tr><td colspan="5" class="text-center text-muted"><?= show('Carregando...') ?></td></tr>
            </tbody>
        </table>
    </div>

    <div style="display:flex; justify-content:flex-end; gap:.5rem; margin-top:1rem;">
        <button type="button" class="btn btn-light" data-bs-dismiss="modal"><?= show('Cancelar') ?></button>
        <button type="button" class="btn btn-success" id="rmw-enviar"
                data-acao="<?= htmlspecialchars($acaoEnviar) ?>">
            <?= show('Enviar') ?> <span id="rmw-enviar-n"></span>
        </button>
    </div>
</div>

<script>
(function () {
    var missaoId = <?= $missaoId ?>;
    var base = '/labs/apis/Logistica/AlocacaoWms/RevisarMovimentosLaboratorio/';
    var tbody = document.getElementById('rmw-tbody');

    function atualizarContagem() {
        var ligados = tbody.querySelectorAll('input.rmw-switch:checked').length;
        var total = tbody.querySelectorAll('input.rmw-switch').length;
        document.getElementById('rmw-count').textContent = ligados + '/' + total;
        document.getElementById('rmw-enviar-n').textContent = '(' + ligados + ')';
    }

    function endereco(m) {
        if (m.origem && m.destino) { return m.origem + ' → ' + m.destino; }
        return m.localizacao || '—';
    }

    function render(movs) {
        if (!movs.length) {
            tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted"><?= show('Nenhum movimento para revisar.') ?></td></tr>';
            atualizarContagem();
            return;
        }
        tbody.innerHTML = movs.map(function (m) {
            var prod = m.produto_nome + (m.produto_codigo ? ' (' + m.produto_codigo + ')' : '');
            var qtd = (m.quantidade === null || m.quantidade === undefined) ? '—' : m.quantidade;
            return '<tr data-id="' + m.id + '">' +
                '<td>' + prod + '</td>' +
                '<td>' + endereco(m) + '</td>' +
                '<td class="text-end">' + qtd + '</td>' +
                '<td class="text-muted small">' + (m.confirmada_em || '') + '</td>' +
                '<td class="text-center"><div class="form-check form-switch d-inline-block">' +
                '<input class="form-check-input rmw-switch" type="checkbox" data-id="' + m.id + '"' + (m.ligado ? ' checked' : '') + '></div></td>' +
                '</tr>';
        }).join('');
        tbody.querySelectorAll('input.rmw-switch').forEach(function (sw) {
            sw.addEventListener('change', function () {
                fetch(base + 'alternar', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ movimento_id: this.dataset.id, ligado: this.checked ? 1 : 0 })
                }).then(function (r) { return r.json(); }).catch(function () {});
                atualizarContagem();
            });
        });
        atualizarContagem();
    }

    function carregar() {
        fetch(base + 'listar?missao_id=' + missaoId)
            .then(function (r) { return r.json(); })
            .then(function (d) { render((d && d.movimentos) || []); })
            .catch(function () {
                tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger"><?= show('Erro ao carregar.') ?></td></tr>';
            });
    }

    function setAll(estado) {
        var alteracoes = [];
        tbody.querySelectorAll('input.rmw-switch').forEach(function (sw) {
            if (sw.checked !== estado) {
                sw.checked = estado;
                alteracoes.push(sw.dataset.id);
            }
        });
        alteracoes.forEach(function (id) {
            fetch(base + 'alternar', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ movimento_id: id, ligado: estado ? 1 : 0 })
            }).catch(function () {});
        });
        atualizarContagem();
    }

    document.getElementById('rmw-all-on').addEventListener('click', function () { setAll(true); });
    document.getElementById('rmw-all-off').addEventListener('click', function () { setAll(false); });

    // "Enviar": dispara a ação canônica (já registrada no botão via data-acao).
    document.getElementById('rmw-enviar').addEventListener('click', function () {
        var ev = document.createElement('a');
        ev.setAttribute('href', 'javascript:void(0)');
        ev.setAttribute('class', 'd-none');
        ev.setAttribute('onclick', this.dataset.acao);
        document.body.appendChild(ev);
        ev.click();
        document.body.removeChild(ev);
    });

    carregar();
})();
</script>
```

> **Nota de execução:** o disparo do "Enviar" usa o mesmo `acaoElemento(...)` dos botões de aprovação da lista (`designacoes_lista.php`). Ao implementar, confirme no `designacoes_lista.js`/framework como a ação-elemento é efetivamente disparada e replique o padrão exato (pode ser um atributo `onclick`/`data-*` consumido pelo framework, em vez do truque do `<a>` acima). O objetivo: chamar `MissoesAprovacao::aprovarOperacaoWms` com `missao_id` e, no sucesso, fechar a modal e remover a linha (o `removerElemento` do backend já cuida da remoção).

- [ ] **Step 2: Adicionar o método controller**

Em `Designacoes.php`, adicionar o método (espelha `detalhesFaltas`, ~linha 1520):

```php
    /**
     * Modal de revisão de movimentos WMS (aprovação por movimento).
     */
    public function revisarMovimentosWms(?array $data = [])
    {
        $this->galaxiaRoute->showOnView(false);

        $missaoId = (int)($data['missao_id'] ?? $_POST['missao_id'] ?? $_GET['missao_id'] ?? 0);

        $resposta = [];
        $resposta['html'] = $this->galaxiaRoute->visual('forms/revisar_movimentos_wms', ['missao_id' => $missaoId], []);
        $this->galaxiaRoute->addResponse($this->routeId, $resposta);
        $this->response = $this->galaxiaRoute->getResponse($this->routeId);

        return $this;
    }
```

- [ ] **Step 3: Adicionar o botão na lista (branch ALOCACAO_WMS)**

Em `designacoes_lista.php`, na seção dos botões de aprovação (~1385-1416), adicionar um branch ANTES do branch genérico "Other Types". Use o `id()` do tipo (ALOCACAO_WMS = 14) já disponível via `$m->tipo_missao_id`:

```php
                <?php if ((int)$m->tipo_missao_id === \GalaxiaConstelacoes\Logistica\MissaoLogistica\Enums\TipoMissao::ALOCACAO_WMS->id()): ?>
                <button class="btn-icon success"
                    <?= $galaxia->abrirModal('revisarMovimentosWms', ['sendData' => ['missao_id' => $m->id], 'modalSize' => 'modal-xl modal-dialog-centered'])['html']; ?>
                    title="<?= show('Revisar movimentos') ?>">
                    <i class="bi bi-list-check"></i>
                </button>
                <?php else: ?>
                <!-- ... branch genérico existente (aprovarMissao) permanece aqui ... -->
                <?php endif; ?>
```

> **Nota de execução:** encaixe o `if/else` em volta do branch genérico existente (o que usa `aprovarMissao`), sem remover os branches de Carregamento/Separação que vêm antes. Confirme a variável de `$galaxia` disponível no escopo do arquivo (a lista já usa `$galaxia->setConstelacao(...)` e `$galaxia->abrirModal(...)`).

- [ ] **Step 4: Smoke backend (a modal renderiza algo)**

Run (verifica que o Lab listar responde JSON p/ uma missão semeada — reutilize o teste da Task 6):
`php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/RevisarMovimentosLaboratorioTddTest.php`
Expected: PASS (o backend que a modal consome está verde).
Run: `php -l componentes/Missoes/Logistica/sinais/Designacoes/forms/revisar_movimentos_wms.php`
Expected: `No syntax errors detected`.

- [ ] **Step 5: Smoke no browser (manual)**

1. Criar uma missão WMS com "Requer Aprovação" marcado (após a Task 8 estar aplicada) e registrar 2-3 movimentos como operador.
2. Finalizar a missão (vai p/ AGUARDANDO_APROVACAO).
3. Na lista de designações, clicar "Revisar movimentos" → a modal lista os movimentos com switches ON.
4. Desligar 1, "Enviar" → a missão some da lista; conferir no banco: 1 `cancelada`, resto `aprovado`; rodar o cron e ver que só os aprovados sobem.

- [ ] **Step 6: Commit** (só com aprovação)

```bash
git add componentes/Missoes/Logistica/sinais/Designacoes/forms/revisar_movimentos_wms.php \
        componentes/Missoes/Logistica/sinais/Designacoes/Designacoes.php \
        componentes/Missoes/Logistica/sinais/Designacoes/html/designacoes_lista.php
git commit -m "feat(operacao-wms): modal de revisão de movimentos WMS na lista de designações"
```

---

### Task 8: Ativação — destravar o "Requer Aprovação" em WMS

> **Último passo de propósito:** até aqui o sistema permanece idêntico ao atual (WMS auto-conclui, `requer_aprovacao` forçado a `0`, carimbo `pendente` dormente). Este passo "liga" a feature ponta a ponta.

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Designacoes/Laboratorios/GestaoMissoesAlocacaoWms.php` (`processarEnvio` linha ~31-32)
- Test: `componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php` (Modify — adicionar caso)

**Interfaces:**
- Produces: missão WMS persiste `requer_aprovacao` conforme o checkbox do painel (default marcado), em vez de forçar `0`.

- [ ] **Step 1: Adicionar o caso de teste (falhando)**

No `GestaoMissoesAlocacaoWmsTddTest.php`, adicionar um caso que cria uma missão WMS com `requer_aprovacao='1'` no input e verifica que a missão persistida tem `requer_aprovacao = 1` (e outro com `'0'` → 0). Siga o padrão existente do arquivo para montar `$data` e ler a missão criada. Asserção central:

```php
// requer_aprovacao deve refletir o input (não mais forçado a 0)
check((int)$missaoCriadaComAprovacao->requer_aprovacao === 1, 'WMS respeita requer_aprovacao=1 do painel');
check((int)$missaoCriadaSemAprovacao->requer_aprovacao === 0, 'WMS respeita requer_aprovacao=0 do painel');
```

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

Run: `php componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php`
Expected: FAIL no caso `requer_aprovacao=1` (ainda forçado a 0).

- [ ] **Step 3: Remover o force-off**

Em `GestaoMissoesAlocacaoWms.php`, no `processarEnvio` (~31-32), remover a linha que força o valor:

```php
        // (REMOVER) Alocação WMS vai direto ao operador — sem gate de aprovação.
        // $data['requer_aprovacao'] = '0';
```

Resultado: o valor do checkbox (`input-requer-aprovacao`) flui pela persistência normal de `GestaoMissoesBase`. Atualizar também o docblock da classe (linha ~13-15) que afirma "requer_aprovacao=0".

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

Run: `php componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php`
Expected: PASS (incl. os 2 casos novos; baseline 15+ mantidos).

- [ ] **Step 5: Commit** (só com aprovação)

```bash
git add componentes/Missoes/Logistica/sinais/Designacoes/Laboratorios/GestaoMissoesAlocacaoWms.php \
        componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php
git commit -m "feat(operacao-wms): destravar Requer Aprovação para missões WMS (ativação)"
```

---

### Task 9: Regressão + verificação ponta a ponta

**Files:** nenhum (só execução).

- [ ] **Step 1: Rodar a suíte WMS completa**

Run, um por um, e conferir 0 FAIL:
```bash
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Enums/AprovacaoGestorTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaAprovacaoTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaRevisaoTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/ConfirmarAlocacaoLaboratorioTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/ConfirmarAlocacaoAprovacaoTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/AprovarOperacaoWmsTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/RevisarMovimentosLaboratorioTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/Operacao/AlocacaoAdapterTddTest.php
php componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php
```
Expected: todos `TODOS OS TESTES PASSARAM`. Garantir que nenhum deixou resíduo (os `finally` limpam `wms_movimentos`, `wms_localizacoes`, `missao_logistica*` de teste).

- [ ] **Step 2: Conferir ausência de resíduo no banco**

Run:
```bash
php -r '
$_SERVER["HTTP_HOST"]="dev-genesis.goldieapp.com.br"; require "autoload.php";
$pdo=(new Galaxia\_Controladores\Connect)->getInstance(null,null,"dev_gx_goldie");
echo "locs teste: ".$pdo->query("SELECT COUNT(*) FROM wms_localizacoes WHERE codigo LIKE \x27__test_%\x27")->fetchColumn().PHP_EOL;
'
```
Expected: `locs teste: 0`.

- [ ] **Step 3: Atualizar memória + techplan (status da fase)**

- Atualizar `PRD/operacao-wms/02-techplan.md` registrando a nova capacidade (aprovação por movimento) como fase concluída.
- Atualizar a memória `tms_curadoria_alertas`? Não — sem relação. Criar/atualizar memória de projeto sobre o gate `aprovacao_gestor` se o usuário pedir.

## Self-Review (preenchido pelo autor do plano)

- **Cobertura da spec:** §3 (migration/coluna)→T1; §4 (máquina de estados)→T2/T3/T4/T5; §5 (gate 3 pontos)→T2; §6 (carimbar pendente)→T4; §7 (destravar criação)→T8; §8 (tela do gestor)→T7; §9 (Lab backend)→T5+T6; §10 (não-regressão)→T9 + defaults; §11 (testes)→T1-T6,T9; §12 (fora de escopo)→respeitado (sem devolver ao operador, sem múltiplas rodadas). ✅ sem lacunas.
- **Placeholders:** nenhum passo de código sem código; "Notas de execução" marcam pontos a confirmar no código real (valor do enum StatusMissao, mecanismo exato da ação-elemento, encaixe do if/else na lista) — são verificações, não lacunas.
- **Consistência de tipos:** `registrarMovimento(..., AprovacaoGestor $aprovacao=APROVADO)`, `listarParaRevisao`/`alternarAprovacao`/`commitAprovacao`, `aprovarOperacaoWms`, `RevisarMovimentosLaboratorio` (listar/alternar) — nomes idênticos em todas as tasks que os consomem. ✅
