# Alocação WMS — Fix de escopo da remoção + push do estorno — 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:** Corrigir o "Nada a remover neste endereço" (remoção passa a ser por endereço, cross-missão) e propagar o cancelamento ao ERP/WMS via o worker assíncrono, apenas para movimentos já enviados.

**Architecture:** A remoção (soft-delete append-only) deixa de ser escopada por missão e passa a casar com o painel (cross-missão). Ao cancelar, cada canal de integração que estava `enviado` é marcado `estorno_pendente`; o cron existente ganha uma 2ª varredura que empurra o estorno pelos hooks (mesmo endpoint stub) e marca `estornado`/`estorno_falha`.

**Tech Stack:** PHP 8.x, Sondas V2 (`ModelInteligencia`), Postgres (`dev_gx_goldie` p/ testes), cron CLI, hooks HTTP.

## Global Constraints

- **Sem raw SQL nas Sondas:** usar `update()`/`find()` do model (memória `feedback_no_raw_sql`). Exceção já existente: a query de descoberta de blocks no cron é leitura raw permitida.
- **Sem migration de dados:** `status`/`integracao_erp`/`integracao_wms` são `varchar(20)`; os novos valores cabem.
- **Multi-tenant:** Sondas têm `customerControl=true` — testes precisam de `$_SESSION['galaxiaUserInfo']` E `$_SESSION['dashboard']['galaxiaUserInfo']` com `customerData->id` (memória `integration_test_session_groups`).
- **Testes de integração só em `dev_gx_goldie`** e com cleanup no `finally` (sem lixo no banco).
- **Commits:** este projeto **não commita sem pedido explícito do usuário** (memória `feedback_no_autocommit`). Os passos "Commit" abaixo só rodam se o usuário autorizar; caso contrário, fazer apenas `git add` e seguir.
- **Estorno só quando o canal estava `enviado`** (por canal ERP/WMS independente). Nunca-enviado não gera push.
- **opc de estorno é provisório** (`cancelar_localizacao_wms`) — endpoint externo é stub.

---

### Task 1: Estados de estorno no enum `EstadoIntegracao`

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Enums/EstadoIntegracao.php`
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Enums/EstadoIntegracaoTddTest.php` (criar)

**Interfaces:**
- Consumes: nada.
- Produces: `EstadoIntegracao::ESTORNO_PENDENTE` (`'estorno_pendente'`), `EstadoIntegracao::ESTORNADO` (`'estornado'`), `EstadoIntegracao::ESTORNO_FALHA` (`'estorno_falha'`); cada um com `->label()`.

- [ ] **Step 1: Escrever o teste que falha**

Criar `Testes/Enums/EstadoIntegracaoTddTest.php`:

```php
<?php
if (php_sapi_name() !== 'cli') { die("Apenas via CLI\n"); }
require_once __DIR__ . '/../../../../../../../autoload.php';

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

$falhas = 0;
function check(bool $c, string $m): void { global $falhas; echo ($c ? "[PASS] " : "[FAIL] ") . $m . "\n"; if (!$c) { $GLOBALS['falhas'] = ($GLOBALS['falhas'] ?? 0) + 1; } }

check(EstadoIntegracao::ESTORNO_PENDENTE->value === 'estorno_pendente', 'ESTORNO_PENDENTE = estorno_pendente');
check(EstadoIntegracao::ESTORNADO->value === 'estornado', 'ESTORNADO = estornado');
check(EstadoIntegracao::ESTORNO_FALHA->value === 'estorno_falha', 'ESTORNO_FALHA = estorno_falha');
check(EstadoIntegracao::ESTORNO_PENDENTE->label() === 'Estorno pendente', 'label ESTORNO_PENDENTE');
check(EstadoIntegracao::ESTORNADO->label() === 'Estornado', 'label ESTORNADO');
check(EstadoIntegracao::ESTORNO_FALHA->label() === 'Estorno falhou', 'label ESTORNO_FALHA');

echo $falhas ? "\nFAIL={$falhas}\n" : "\nTODOS OS TESTES PASSARAM\n";
exit($falhas ? 1 : 0);
```

- [ ] **Step 2: Rodar e ver falhar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Enums/EstadoIntegracaoTddTest.php`
Expected: erro fatal (`ESTORNO_PENDENTE` não existe) ou FAILs.

- [ ] **Step 3: Implementar os cases + labels**

Em `EstadoIntegracao.php`, dentro do enum, adicionar após `case FALHA`:

```php
    case ESTORNO_PENDENTE = 'estorno_pendente';
    case ESTORNADO        = 'estornado';
    case ESTORNO_FALHA    = 'estorno_falha';
```

E no `match` de `label()`, adicionar antes do fechamento:

```php
            self::ESTORNO_PENDENTE => 'Estorno pendente',
            self::ESTORNADO        => 'Estornado',
            self::ESTORNO_FALHA    => 'Estorno falhou',
```

Atualizar também o docblock do enum mencionando os 3 estados de estorno.

- [ ] **Step 4: Rodar e ver passar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Enums/EstadoIntegracaoTddTest.php`
Expected: `TODOS OS TESTES PASSARAM`

- [ ] **Step 5: Commit** (só se autorizado — ver Global Constraints)

```bash
git add componentes/Missoes/Logistica/sinais/AlocacaoWms/Enums/EstadoIntegracao.php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Enums/EstadoIntegracaoTddTest.php
git commit -m "feat(alocacao-wms): estados de estorno no EstadoIntegracao"
```

---

### Task 2: Sonda — cancelamento cross-missão + agendamento e fila de estorno

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Sondas/WmsMovimentoSonda.php`
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaEstornoTddTest.php` (criar)

**Interfaces:**
- Consumes: `EstadoIntegracao::ESTORNO_PENDENTE|ESTORNO_FALHA|ENVIADO|PENDENTE` (Task 1); `StatusAlocacao::CONFIRMADA|CANCELADA`.
- Produces:
  - `cancelarPorProdutoLocalizacao(int $produtoId, string $localizacaoCodigo): int` — cancela cross-missão (tenant) e agenda estorno por canal; retorna nº de movimentos cancelados.
  - `buscarPendentesEstornoErp(int $limite = 100): array` / `buscarPendentesEstornoWms(int $limite = 100): array` — linhas `status=cancelada` com `integracao_*` em estado de estorno.
  - **Mantém** `cancelarPorMissaoProdutoLocalizacao` por enquanto (removido na Task 3, junto com a troca do único chamador — evita estado quebrado entre tasks).

- [ ] **Step 1: Escrever o teste de integração que falha**

Criar `Testes/Sondas/WmsMovimentoSondaEstornoTddTest.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\EstadoIntegracao;
use GalaxiaMissoes\Logistica\sinais\AlocacaoWms\Enums\OperacaoTipo;

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
if ($pdo->query('SELECT current_database()')->fetchColumn() !== 'dev_gx_goldie') { fwrite(STDERR, "ABORT\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";} }

$BLOCK=22; $OP=1; $PROD=15424; $PFX='__esttest_';
$LA=$PFX.'la'; $LB=$PFX.'lb';
$sonda = new WmsMovimentoSonda();

// helper: lê uma linha por id
$row = fn(int $id) => $pdo->query("SELECT * FROM wms_movimentos WHERE id={$id}")->fetch();

try {
    // Fixtures via PDO: duas missões fake distintas (A=900001, B=900002) — só números,
    // a Sonda não valida missão. Movimentos no MESMO endereço LA em missões diferentes.
    $pdo->exec("DELETE FROM wms_movimentos WHERE localizacao_codigo LIKE '{$PFX}%'");

    // m1: enviado nos dois canais (deve virar estorno_pendente nos dois)
    $pdo->exec("INSERT INTO wms_movimentos (missao_logistica_id,produto_id,localizacao_codigo,quantidade,origem_leitura,status,operacao_tipo,operador_id,confirmada_em,integracao_erp,integracao_wms,block) VALUES (900001,{$PROD},'{$LA}',2,'manual','confirmada','alocacao',{$OP},NOW(),'enviado','enviado',{$BLOCK})");
    $ID1=(int)$pdo->query("SELECT id FROM wms_movimentos WHERE localizacao_codigo='{$LA}' ORDER BY id DESC LIMIT 1")->fetchColumn();
    // m2 (OUTRA missão, mesmo endereço): nunca enviado (pendente nos dois) → NÃO vira estorno
    $pdo->exec("INSERT INTO wms_movimentos (missao_logistica_id,produto_id,localizacao_codigo,quantidade,origem_leitura,status,operacao_tipo,operador_id,confirmada_em,integracao_erp,integracao_wms,block) VALUES (900002,{$PROD},'{$LA}',3,'manual','confirmada','alocacao',{$OP},NOW(),'pendente','pendente',{$BLOCK})");
    $ID2=(int)$pdo->query("SELECT id FROM wms_movimentos WHERE localizacao_codigo='{$LA}' ORDER BY id DESC LIMIT 1")->fetchColumn();

    // --- cancelarPorProdutoLocalizacao: cross-missão ---
    $cancelados = $sonda->cancelarPorProdutoLocalizacao($PROD, $LA);
    check($cancelados === 2, "cancela cross-missão as 2 linhas (ret=2, veio {$cancelados})");
    check($row($ID1)->status === 'cancelada' && $row($ID2)->status === 'cancelada', "ambas viraram CANCELADA");

    // m1 (era enviado) → estorno_pendente nos dois canais
    check($row($ID1)->integracao_erp === 'estorno_pendente', "m1 ERP enviado→estorno_pendente");
    check($row($ID1)->integracao_wms === 'estorno_pendente', "m1 WMS enviado→estorno_pendente");
    // m2 (nunca enviado) → segue pendente (sem estorno)
    check($row($ID2)->integracao_erp === 'pendente', "m2 ERP pendente continua pendente (sem estorno)");
    check($row($ID2)->integracao_wms === 'pendente', "m2 WMS pendente continua pendente (sem estorno)");

    // --- buscarPendentesEstorno* ---
    $estErp = array_map(fn($r)=>(int)$r->id, $sonda->buscarPendentesEstornoErp(100));
    check(in_array($ID1,$estErp,true) && !in_array($ID2,$estErp,true), "buscarPendentesEstornoErp traz m1, não m2");
    $estWms = array_map(fn($r)=>(int)$r->id, $sonda->buscarPendentesEstornoWms(100));
    check(in_array($ID1,$estWms,true) && !in_array($ID2,$estWms,true), "buscarPendentesEstornoWms traz m1, não m2");

    // idempotência: cancelar de novo não recancela (0) e não regride estados
    $c2 = $sonda->cancelarPorProdutoLocalizacao($PROD, $LA);
    check($c2 === 0, "2ª chamada não cancela nada (idempotente)");
    check($row($ID1)->integracao_erp === 'estorno_pendente', "m1 segue estorno_pendente após 2ª chamada");

    // canais independentes: linha enviado só no WMS
    $pdo->exec("INSERT INTO wms_movimentos (missao_logistica_id,produto_id,localizacao_codigo,quantidade,origem_leitura,status,operacao_tipo,operador_id,confirmada_em,integracao_erp,integracao_wms,block) VALUES (900003,{$PROD},'{$LB}',1,'manual','confirmada','alocacao',{$OP},NOW(),'pendente','enviado',{$BLOCK})");
    $ID3=(int)$pdo->query("SELECT id FROM wms_movimentos WHERE localizacao_codigo='{$LB}' ORDER BY id DESC LIMIT 1")->fetchColumn();
    $sonda->cancelarPorProdutoLocalizacao($PROD, $LB);
    check($row($ID3)->integracao_erp === 'pendente', "canal independente: ERP nunca-enviado fica pendente");
    check($row($ID3)->integracao_wms === 'estorno_pendente', "canal independente: WMS enviado→estorno_pendente");

} finally {
    $pdo->exec("DELETE FROM wms_movimentos WHERE localizacao_codigo LIKE '{$PFX}%'");
}

echo "\nRESULTADO PASS={$passou} FAIL={$falhas}\n";
exit($falhas ? 1 : 0);
```

- [ ] **Step 2: Rodar e ver falhar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaEstornoTddTest.php`
Expected: erro fatal (`cancelarPorProdutoLocalizacao` não existe).

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

Em `WmsMovimentoSonda.php`, **adicionar** os métodos abaixo (mantendo `cancelarPorMissaoProdutoLocalizacao` por enquanto — ele será removido na Task 3 junto com a troca do chamador):

```php
    /**
     * Cancela (soft-delete) TODOS os movimentos CONFIRMADOS de um produto num endereço,
     * CROSS-MISSÃO no tenant atual — "remover produto do endereço" (voltar atrás).
     *
     * Espelha o painel (consultarEndereco é cross-missão). Append-only: as linhas viram
     * CANCELADA. Em seguida AGENDA o estorno externo por canal: o que já foi 'enviado'
     * vira 'estorno_pendente' (o cron empurra a reversão); o que nunca foi enviado fica
     * como está e o cron de registro para de empurrar (gate por status).
     *
     * Idempotente: re-chamar não recancela nem regride estados de estorno.
     *
     * @return int Número de movimentos cancelados nesta chamada (0 = nada a cancelar).
     */
    public function cancelarPorProdutoLocalizacao(int $produtoId, string $localizacaoCodigo): int
    {
        $confirmada = StatusAlocacao::CONFIRMADA->value;
        $cancelada  = StatusAlocacao::CANCELADA->value;
        $enviado    = EstadoIntegracao::ENVIADO->value;
        $estorno    = EstadoIntegracao::ESTORNO_PENDENTE->value;

        // 1. Cancela (cross-missão; customerControl injeta o tenant).
        $cancelados = (int)$this->update(
            ['status' => $cancelada],
            'produto_id = :p AND localizacao_codigo = :c AND status = :s',
            "p={$produtoId}&c={$localizacaoCodigo}&s={$confirmada}"
        );

        // 2/3. Agenda estorno só do que já foi enviado, por canal (idempotente:
        //      'enviado' AND 'cancelada' = enviado e depois cancelado, sem estorno ainda).
        $this->update(
            ['integracao_erp' => $estorno],
            'produto_id = :p AND localizacao_codigo = :c AND status = :s AND integracao_erp = :e',
            "p={$produtoId}&c={$localizacaoCodigo}&s={$cancelada}&e={$enviado}"
        );
        $this->update(
            ['integracao_wms' => $estorno],
            'produto_id = :p AND localizacao_codigo = :c AND status = :s AND integracao_wms = :e',
            "p={$produtoId}&c={$localizacaoCodigo}&s={$cancelada}&e={$enviado}"
        );

        return $cancelados;
    }

    /**
     * Fila de estorno ERP: movimentos cancelados que precisam avisar o ERP da reversão.
     * Gate por status=cancelada + integracao_erp em estado de estorno (pendente/falha).
     *
     * @return array<int,object>
     */
    public function buscarPendentesEstornoErp(int $limite = 100): array
    {
        $cancelada = StatusAlocacao::CANCELADA->value;
        $pend      = EstadoIntegracao::ESTORNO_PENDENTE->value;
        $falha     = EstadoIntegracao::ESTORNO_FALHA->value;

        $rows = $this->find(
            "status = '{$cancelada}' AND integracao_erp IN ('{$pend}', '{$falha}')",
            ''
        )->order('confirmada_em ASC')->fetch(true);

        return $rows ? array_slice($rows, 0, $limite) : [];
    }

    /**
     * Fila de estorno WMS (análoga à ERP, canal independente).
     *
     * @return array<int,object>
     */
    public function buscarPendentesEstornoWms(int $limite = 100): array
    {
        $cancelada = StatusAlocacao::CANCELADA->value;
        $pend      = EstadoIntegracao::ESTORNO_PENDENTE->value;
        $falha     = EstadoIntegracao::ESTORNO_FALHA->value;

        $rows = $this->find(
            "status = '{$cancelada}' AND integracao_wms IN ('{$pend}', '{$falha}')",
            ''
        )->order('confirmada_em ASC')->fetch(true);

        return $rows ? array_slice($rows, 0, $limite) : [];
    }
```

- [ ] **Step 4: Rodar e ver passar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaEstornoTddTest.php`
Expected: `RESULTADO PASS=11 FAIL=0`

- [ ] **Step 5: Garantir que a Sonda existente não quebrou**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaTddTest.php`
Expected: `TODOS OS TESTES PASSARAM` (regressão da Sonda).

- [ ] **Step 6: Commit** (só se autorizado)

```bash
git add componentes/Missoes/Logistica/sinais/AlocacaoWms/Sondas/WmsMovimentoSonda.php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaEstornoTddTest.php
git commit -m "feat(alocacao-wms): cancelamento cross-missão + fila de estorno na Sonda"
```

---

### Task 3: Laboratório — remoção por endereço (fix do bug)

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/RemoverAlocacaoLaboratorio.php:64`
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/RemoverAlocacaoLaboratorioTddTest.php` (já existe — atualizar S2)

**Interfaces:**
- Consumes: `WmsMovimentoSonda::cancelarPorProdutoLocalizacao(int,string): int` (Task 2).
- Produces: `removerProdutoDoEndereco` agora remove cross-missão (mantém assinatura).

- [ ] **Step 1: Confirmar que não há outros chamadores do método antigo**

Run: `grep -rn "cancelarPorMissaoProdutoLocalizacao" componentes/ --include=*.php`
Expected: nenhuma referência fora de eventuais testes antigos (a Sonda já removeu na Task 2). Se aparecer em outro teste, migrar/remover esse teste.

- [ ] **Step 2: Atualizar o teste de reprodução para esperar o comportamento corrigido**

Em `RemoverAlocacaoLaboratorioTddTest.php`, no bloco **S2**, trocar a asserção `expectFixed(... !empty($r2['ok']) ...)` (que reproduzia o bug) por asserções do comportamento corrigido:

```php
    // S2 — corrigido: remover um produto VISÍVEL no painel funciona mesmo "estando em outra missão".
    expectFixed(
        !empty($r2['ok']) && ($r2['removidos'] ?? 0) >= 1,
        "S2: remover produto visível funciona cross-missão (ok + removidos>=1)",
        "Regressão: removerProdutoDoEndereco devolveu " . json_encode($r2, JSON_UNESCAPED_UNICODE)
    );
    check(consultaMostraProduto($consult, $L_S2, $PRODUTO_ID) === false,
        "S2: após remover cross-missão, o produto some do painel");
    check(contarConfirmados($pdo, $PRODUTO_ID, $L_S2, $BLOCK) === 0,
        "S2: nenhum movimento CONFIRMADA restou no endereço");
```

(Remover a antiga asserção `check($erro2 === 'Nada a remover neste endereço.' ...)` e a `check(contarConfirmados(...) === 1 ...)` do S2, que afirmavam o bug.)

- [ ] **Step 3: Rodar e ver falhar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/RemoverAlocacaoLaboratorioTddTest.php 2>&1 | grep -E "^\[(PASS|BUG|FAIL)\]|RESULTADO"`
Expected: S2 falha (`[BUG ]`) porque o Laboratório ainda chama o método por-missão (removido na Task 2 → erro/0).

- [ ] **Step 4: Trocar a chamada no Laboratório**

Em `RemoverAlocacaoLaboratorio.php`, substituir a linha 64:

```php
        $removidos = $this->alocacaoSonda->cancelarPorMissaoProdutoLocalizacao($missaoId, $produtoId, $localizacaoCodigo);
```

por:

```php
        // Cross-missão: o painel do endereço é cross-missão, a remoção também precisa ser.
        // missaoId segue usado para permissão (acima) e extrato (abaixo).
        $removidos = $this->alocacaoSonda->cancelarPorProdutoLocalizacao($produtoId, $localizacaoCodigo);
```

Atualizar o docblock da classe/método removendo "dentro da missão atual" e explicando o escopo por endereço + agendamento de estorno.

- [ ] **Step 5: Rodar e ver passar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/RemoverAlocacaoLaboratorioTddTest.php 2>&1 | grep -E "^\[(PASS|BUG|FAIL)\]|RESULTADO"`
Expected: S1 e S2 PASS; `BUGS reproduzidos=0`; `FAIL=0`. (S3/S4/S5/S6 seguem como NOTE/caracterização.)

- [ ] **Step 6: Remover o método antigo (agora sem chamadores)**

Confirmar de novo que ninguém mais usa o método por-missão:

Run: `grep -rn "cancelarPorMissaoProdutoLocalizacao" componentes/ --include=*.php`
Expected: nenhuma referência.

Então **remover** o método `cancelarPorMissaoProdutoLocalizacao` de `WmsMovimentoSonda.php` (código morto após a migração do chamador).

- [ ] **Step 7: Rodar a Sonda + reprodução de novo (regressão)**

Run:
```
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaEstornoTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/RemoverAlocacaoLaboratorioTddTest.php
```
Expected: ambos verdes (a remoção do método morto não quebra nada).

- [ ] **Step 8: Commit** (só se autorizado)

```bash
git add componentes/Missoes/Logistica/sinais/AlocacaoWms/Laboratorios/RemoverAlocacaoLaboratorio.php componentes/Missoes/Logistica/sinais/AlocacaoWms/Sondas/WmsMovimentoSonda.php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/RemoverAlocacaoLaboratorioTddTest.php
git commit -m "fix(alocacao-wms): remover produto do endereço cross-missão (corrige 'Nada a remover')"
```

---

### Task 4: Adapters — opc/payload de estorno (default no base)

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Strategies/Operacao/OperacaoAdapterInterface.php`
- Modify: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Strategies/Operacao/OperacaoAdapterBase.php`
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/Operacao/EstornoPayloadTddTest.php` (criar)

**Interfaces:**
- Consumes: `OperacaoAdapterBase::payloadPushErp/Wms`, `operacaoPushErp/Wms` (existentes).
- Produces (em todos os adapters via base): `operacaoPushErpEstorno(): string`, `operacaoPushWmsEstorno(): string`, `payloadPushErpEstorno(array,int): array`, `payloadPushWmsEstorno(array,int): array`. Payload de estorno = payload normal com `opc` trocado + `'acao' => 'estorno'`.

- [ ] **Step 1: Escrever o teste que falha**

Criar `Testes/Strategies/Operacao/EstornoPayloadTddTest.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\Strategies\Operacao\AlocacaoAdapter;

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

$a = new AlocacaoAdapter();
$item = ['alocacao_id'=>10,'produto_id'=>5,'importacao_id'=>'abc','localizacao_codigo'=>'A-1','quantidade'=>2,'operador_id'=>1];

check($a->operacaoPushErpEstorno() === 'cancelar_localizacao_wms', 'opc estorno ERP');
check($a->operacaoPushWmsEstorno() === 'cancelar_localizacao_wms', 'opc estorno WMS');

$pErp = $a->payloadPushErpEstorno($item, 99);
check(($pErp['opc'] ?? '') === 'cancelar_localizacao_wms', 'payload ERP: opc = estorno');
check(($pErp['acao'] ?? '') === 'estorno', 'payload ERP: acao = estorno');
check((int)($pErp['alocacao_id'] ?? 0) === 10, 'payload ERP: mantém alocacao_id');

$pWms = $a->payloadPushWmsEstorno($item, 99);
check(($pWms['opc'] ?? '') === 'cancelar_localizacao_wms', 'payload WMS: opc = estorno');
check(($pWms['acao'] ?? '') === 'estorno', 'payload WMS: acao = estorno');

echo $falhas ? "\nFAIL={$falhas}\n" : "\nTODOS OS TESTES PASSARAM\n";
exit($falhas ? 1 : 0);
```

- [ ] **Step 2: Rodar e ver falhar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/Operacao/EstornoPayloadTddTest.php`
Expected: erro fatal (`operacaoPushErpEstorno` não existe).

- [ ] **Step 3: Adicionar ao contrato (interface)**

Em `OperacaoAdapterInterface.php`, antes do fechamento da interface, adicionar:

```php
    /** Nome da operação de ESTORNO (reversão do cancelamento) no ERP (campo `opc`). */
    public function operacaoPushErpEstorno(): string;

    /** Nome da operação de ESTORNO na base WMS externa (campo `opc`). */
    public function operacaoPushWmsEstorno(): string;

    /** Payload de estorno para o ERP (reusa payloadPushErp, troca opc + acao=estorno). */
    public function payloadPushErpEstorno(array $item, int $missaoId): array;

    /** Payload de estorno para a base WMS externa. */
    public function payloadPushWmsEstorno(array $item, int $missaoId): array;
```

- [ ] **Step 4: Implementar defaults no base**

Em `OperacaoAdapterBase.php`, antes do `montarMovimento`, adicionar:

```php
    /**
     * Operação de estorno (reversão) — opc provisório comum; o endpoint externo
     * ainda é stub. Operações com reversão própria sobrescrevem.
     */
    public function operacaoPushErpEstorno(): string
    {
        return 'cancelar_localizacao_wms';
    }

    public function operacaoPushWmsEstorno(): string
    {
        return 'cancelar_localizacao_wms';
    }

    /**
     * Payload de estorno = payload normal com opc de reversão e acao='estorno'.
     * Reusa payloadPushErp/Wms para herdar a forma do item por operação.
     */
    public function payloadPushErpEstorno(array $item, int $missaoId): array
    {
        $p = $this->payloadPushErp($item, $missaoId);
        $p['opc']  = $this->operacaoPushErpEstorno();
        $p['acao'] = 'estorno';
        return $p;
    }

    public function payloadPushWmsEstorno(array $item, int $missaoId): array
    {
        $p = $this->payloadPushWms($item, $missaoId);
        $p['opc']  = $this->operacaoPushWmsEstorno();
        $p['acao'] = 'estorno';
        return $p;
    }
```

- [ ] **Step 5: Rodar e ver passar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/Operacao/EstornoPayloadTddTest.php`
Expected: `TODOS OS TESTES PASSARAM`

- [ ] **Step 6: Regressão dos adapters existentes**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/Operacao/AlocacaoAdapterTddTest.php`
Expected: continua passando (a interface ganhou métodos com default no base — adapters concretos não precisam mudar).

- [ ] **Step 7: Commit** (só se autorizado)

```bash
git add componentes/Missoes/Logistica/sinais/AlocacaoWms/Strategies/Operacao/OperacaoAdapterInterface.php componentes/Missoes/Logistica/sinais/AlocacaoWms/Strategies/Operacao/OperacaoAdapterBase.php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/Operacao/EstornoPayloadTddTest.php
git commit -m "feat(alocacao-wms): opc/payload de estorno nos adapters de operação"
```

---

### Task 5: Hooks — método `estornar()`

**Files:**
- Modify: `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/ErpAlocacaoWmsHookStrategy.php`
- Modify: `componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/WmsAlocacaoWmsHookStrategy.php`
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/HookEstornoTddTest.php` (criar)

**Interfaces:**
- Consumes: `OperacaoAdapterFactory::createFromConfig/create`, `$adapter->operacaoPush*Estorno()/payloadPush*Estorno()` (Task 4); `WmsMovimentoSonda::marcarIntegracaoErp/Wms` com `EstadoIntegracao::ESTORNADO|ESTORNO_FALHA` (Task 1).
- Produces: `ErpAlocacaoWmsHookStrategy::estornar(array $itens, int $missaoId): void` e `WmsAlocacaoWmsHookStrategy::estornar(array $itens, int $missaoId): void` — empurram a reversão e marcam `estornado`/`estorno_falha`.

- [ ] **Step 1: Escrever o teste de integração que falha**

Criar `Testes/Laboratorios/HookEstornoTddTest.php` (verifica o WIRING — independe da resposta do stub: a linha deve sair de `estorno_pendente`):

```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','name'=>'I'];
$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 GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\ErpAlocacaoWmsHookStrategy;
use GalaxiaConstelacoes\Logistica\MissaoLogistica\Strategies\WmsAlocacaoWmsHookStrategy;

$pdo = (new Connect)->getInstance(null,null,'dev_gx_goldie');
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
$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";}}

$BLOCK=22;$OP=1;$PROD=15424;$PFX='__hooktest_';$L=$PFX.'l';
$row = fn(int $id)=>$pdo->query("SELECT * FROM wms_movimentos WHERE id={$id}")->fetch();

try {
    $pdo->exec("DELETE FROM wms_movimentos WHERE localizacao_codigo LIKE '{$PFX}%'");
    // linha cancelada com estorno pendente nos dois canais
    $pdo->exec("INSERT INTO wms_movimentos (missao_logistica_id,produto_id,localizacao_codigo,quantidade,origem_leitura,status,operacao_tipo,operador_id,confirmada_em,integracao_erp,integracao_wms,block) VALUES (900100,{$PROD},'{$L}',2,'manual','cancelada','alocacao',{$OP},NOW(),'estorno_pendente','estorno_pendente',{$BLOCK})");
    $ID=(int)$pdo->query("SELECT id FROM wms_movimentos WHERE localizacao_codigo='{$L}' ORDER BY id DESC LIMIT 1")->fetchColumn();

    $sonda = new WmsMovimentoSonda();
    $itemErp = ['alocacao_id'=>$ID,'produto_id'=>$PROD,'importacao_id'=>'x','localizacao_codigo'=>$L,'quantidade'=>2,'operador_id'=>$OP,'operacao_tipo'=>'alocacao'];
    (new ErpAlocacaoWmsHookStrategy())->estornar([$itemErp], 900100);
    check($row($ID)->integracao_erp !== 'estorno_pendente', "ERP estornar move o estado (estornado|estorno_falha): ".$row($ID)->integracao_erp);
    check(in_array($row($ID)->integracao_erp, ['estornado','estorno_falha'], true), "ERP estado final válido");

    $itemWms = $itemErp;
    (new WmsAlocacaoWmsHookStrategy())->estornar([$itemWms], 900100);
    check($row($ID)->integracao_wms !== 'estorno_pendente', "WMS estornar move o estado: ".$row($ID)->integracao_wms);
    check(in_array($row($ID)->integracao_wms, ['estornado','estorno_falha'], true), "WMS estado final válido");
} finally {
    $pdo->exec("DELETE FROM wms_movimentos WHERE localizacao_codigo LIKE '{$PFX}%'");
}
echo "\nPASS={$passou} FAIL={$falhas}\n";
exit($falhas?1:0);
```

- [ ] **Step 2: Rodar e ver falhar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/HookEstornoTddTest.php`
Expected: erro fatal (`estornar` não existe nos hooks).

- [ ] **Step 3: Implementar `estornar` no hook ERP**

Em `ErpAlocacaoWmsHookStrategy.php`, adicionar o método (espelha `sincronizar`, mas opc/payload/estado de estorno):

```php
    /**
     * Empurra o ESTORNO (reversão do cancelamento) de uma lista de movimentos ao ERP.
     * Espelha sincronizar(), mas usa o opc/payload de estorno do adapter e marca
     * integracao_erp = 'estornado' (sucesso) ou 'estorno_falha' (retry).
     */
    public function estornar(array $itens, int $missaoId): void
    {
        if (empty($itens)) { return; }
        $labelLog = $this->obterLabelLog();

        foreach ($itens as $item) {
            $importacaoId = $item['importacao_id'] ?? $item['codigomd5'] ?? null;
            $alocacaoId   = (int)($item['alocacao_id'] ?? 0);
            if (empty($importacaoId) || !$alocacaoId) {
                error_log("[{$labelLog}] Estorno: item sem importacao_id ou alocacao_id.");
                continue;
            }

            try {
                $adapter = OperacaoAdapterFactory::createFromConfig(['operacao_tipo' => $item['operacao_tipo'] ?? null]);
            } catch (\Throwable $e) {
                $adapter = OperacaoAdapterFactory::create(OperacaoTipo::ALOCACAO);
            }
            $operacao = $adapter->operacaoPushErpEstorno();
            $payload  = $adapter->payloadPushErpEstorno($item, $missaoId);

            try {
                $resposta = $this->enviarRequisicaoJson($payload, $operacao, $labelLog, $missaoId, $operacao, (string)$importacaoId);
                $sucesso  = ($resposta['status'] ?? '') === 'ok' || ($resposta['ok'] ?? false);
                $estado   = $sucesso ? EstadoIntegracao::ESTORNADO : EstadoIntegracao::ESTORNO_FALHA;
                $erro     = $sucesso ? null : ($resposta['message'] ?? $resposta['erro'] ?? 'Resposta inválida do ERP (estorno)');
                $this->alocacaoSonda->marcarIntegracaoErp($alocacaoId, $estado, $erro);
            } catch (\Throwable $e) {
                error_log("[{$labelLog}] Exceção no estorno alocacao #{$alocacaoId}: " . $e->getMessage());
                $this->alocacaoSonda->marcarIntegracaoErp($alocacaoId, EstadoIntegracao::ESTORNO_FALHA, $e->getMessage());
            }
        }
    }
```

- [ ] **Step 4: Implementar `estornar` no hook WMS**

Em `WmsAlocacaoWmsHookStrategy.php`, adicionar (análogo, canal WMS):

```php
    /**
     * Empurra o ESTORNO de uma lista de movimentos à base WMS externa.
     * Marca integracao_wms = 'estornado' (sucesso) ou 'estorno_falha' (retry).
     */
    public function estornar(array $itens, int $missaoId): void
    {
        if (empty($itens)) { return; }
        $labelLog = $this->obterLabelLog();

        foreach ($itens as $item) {
            $alocacaoId = (int)($item['alocacao_id'] ?? 0);
            $produtoId  = (int)($item['produto_id'] ?? 0);
            if (!$alocacaoId || !$produtoId) {
                error_log("[{$labelLog}] Estorno: item sem alocacao_id ou produto_id.");
                continue;
            }

            try {
                $adapter = OperacaoAdapterFactory::createFromConfig(['operacao_tipo' => $item['operacao_tipo'] ?? null]);
            } catch (\Throwable $e) {
                $adapter = OperacaoAdapterFactory::create(OperacaoTipo::ALOCACAO);
            }
            $operacao = $adapter->operacaoPushWmsEstorno();
            $payload  = $adapter->payloadPushWmsEstorno($item, $missaoId);

            try {
                $resposta = $this->enviarRequisicaoJson($payload, $operacao, $labelLog, $missaoId, $operacao, (string)$alocacaoId);
                $sucesso  = ($resposta['status'] ?? '') === 'ok' || ($resposta['ok'] ?? false);
                $estado   = $sucesso ? EstadoIntegracao::ESTORNADO : EstadoIntegracao::ESTORNO_FALHA;
                $erro     = $sucesso ? null : ($resposta['message'] ?? $resposta['erro'] ?? 'Resposta inválida do WMS (estorno)');
                $this->alocacaoSonda->marcarIntegracaoWms($alocacaoId, $estado, $erro);
            } catch (\Throwable $e) {
                error_log("[{$labelLog}] Exceção no estorno alocacao #{$alocacaoId}: " . $e->getMessage());
                $this->alocacaoSonda->marcarIntegracaoWms($alocacaoId, EstadoIntegracao::ESTORNO_FALHA, $e->getMessage());
            }
        }
    }
```

- [ ] **Step 5: Rodar e ver passar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/HookEstornoTddTest.php`
Expected: `PASS=4 FAIL=0` (estados saíram de `estorno_pendente`).

- [ ] **Step 6: Commit** (só se autorizado)

```bash
git add componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/ErpAlocacaoWmsHookStrategy.php componentes/Constelacoes/Logistica/MissaoLogistica/Strategies/WmsAlocacaoWmsHookStrategy.php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/HookEstornoTddTest.php
git commit -m "feat(alocacao-wms): hooks empurram estorno (reversão) ERP/WMS"
```

---

### Task 6: Cron — 2ª varredura de estorno

**Files:**
- Modify: `crons/wms_alocacao_integracao.php`
- Test: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/CronEstornoSmokeTddTest.php` (criar)

**Interfaces:**
- Consumes: `WmsMovimentoSonda::buscarPendentesEstornoErp/Wms` (Task 2); `hook->estornar()` (Task 5).
- Produces: cron processa cancelados pendentes de estorno além dos confirmados pendentes de registro.

- [ ] **Step 1: Escrever o smoke que falha**

Criar `Testes/Laboratorios/CronEstornoSmokeTddTest.php` (semeia, roda o cron real, confere):

```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 Galaxia\_Controladores\Connect;
$pdo = (new Connect)->getInstance(null,null,'dev_gx_goldie');
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);

$BLOCK=22;$OP=1;$PROD=15424;$PFX='__cronest_';$L=$PFX.'l';
$row = fn(int $id)=>$pdo->query("SELECT * FROM wms_movimentos WHERE id={$id}")->fetch();
$falhas=0; function check(bool $c,string $m):void{global $falhas;echo ($c?"[PASS] ":"[FAIL] ").$m."\n";if(!$c)$falhas++;}

try {
    $pdo->exec("DELETE FROM wms_movimentos WHERE localizacao_codigo LIKE '{$PFX}%'");
    $pdo->exec("INSERT INTO wms_movimentos (missao_logistica_id,produto_id,localizacao_codigo,quantidade,origem_leitura,status,operacao_tipo,operador_id,confirmada_em,integracao_erp,integracao_wms,block) VALUES (900200,{$PROD},'{$L}',2,'manual','cancelada','alocacao',{$OP},NOW(),'estorno_pendente','estorno_pendente',{$BLOCK})");
    $ID=(int)$pdo->query("SELECT id FROM wms_movimentos WHERE localizacao_codigo='{$L}' ORDER BY id DESC LIMIT 1")->fetchColumn();

    // Roda o worker real (CLI)
    $cron = realpath(__DIR__ . '/../../../../../../../crons/wms_alocacao_integracao.php');
    exec('php ' . escapeshellarg($cron) . ' 2>&1', $out, $code);

    check($row($ID)->integracao_erp !== 'estorno_pendente', "cron processou estorno ERP: ".$row($ID)->integracao_erp);
    check($row($ID)->integracao_wms !== 'estorno_pendente', "cron processou estorno WMS: ".$row($ID)->integracao_wms);
} finally {
    $pdo->exec("DELETE FROM wms_movimentos WHERE localizacao_codigo LIKE '{$PFX}%'");
}
echo $falhas ? "\nFAIL={$falhas}\n" : "\nSMOKE OK\n";
exit($falhas?1:0);
```

- [ ] **Step 2: Rodar e ver falhar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/CronEstornoSmokeTddTest.php`
Expected: FAIL (cron ainda ignora cancelados — estados seguem `estorno_pendente`).

- [ ] **Step 3: Incluir estorno na descoberta de tenants**

Em `crons/wms_alocacao_integracao.php`, trocar a `$sqlBlocks` (linhas ~60-68) para também achar tenants com estorno pendente:

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

- [ ] **Step 4: Adicionar os passes de estorno no loop por tenant**

Em `wms_alocacao_integracao.php`, dentro do `try` por tenant, **depois** do bloco "Processar fila WMS" (após a linha ~174, antes do `} catch`), adicionar:

```php
        // --- Processar fila de ESTORNO ERP (cancelados que precisam reverter) ---
        $estornoErp = $sonda->buscarPendentesEstornoErp($LIMITE_POR_CICLO);
        echo "  ESTORNO ERP: " . count($estornoErp) . " encontrados.\n";
        if (!empty($estornoErp)) {
            $itensEstErp = array_map(function ($aloc) {
                $importacaoId = null;
                try {
                    $produtoModel = new Produto();
                    $prod = $produtoModel->findById((int)$aloc->produto_id);
                    if (is_array($prod)) { $prod = $prod[0] ?? null; }
                    $importacaoId = $prod->importacao_id ?? $prod->codigomd5 ?? null;
                } catch (\Throwable $e) {
                    error_log('[wms_worker] estorno: erro produto ' . $aloc->produto_id . ': ' . $e->getMessage());
                }
                return [
                    'alocacao_id'        => (int)$aloc->id,
                    'produto_id'         => (int)$aloc->produto_id,
                    'importacao_id'      => $importacaoId,
                    'localizacao_codigo' => $aloc->localizacao_codigo,
                    'quantidade'         => $aloc->quantidade,
                    'operador_id'        => (int)($aloc->operador_id ?? 0),
                    'confirmada_em'      => $aloc->confirmada_em,
                    'operacao_tipo'      => $aloc->operacao_tipo ?? 'alocacao',
                ];
            }, $estornoErp);
            $porMissaoEstErp = [];
            foreach (array_keys($itensEstErp) as $i) {
                $mid = (int)($estornoErp[$i]->missao_logistica_id ?? 0);
                $porMissaoEstErp[$mid][] = $itensEstErp[$i];
            }
            foreach ($porMissaoEstErp as $missaoId => $itens) {
                $hookErp->estornar($itens, $missaoId);
            }
        }

        // --- Processar fila de ESTORNO WMS ---
        $estornoWms = $sonda->buscarPendentesEstornoWms($LIMITE_POR_CICLO);
        echo "  ESTORNO WMS: " . count($estornoWms) . " encontrados.\n";
        if (!empty($estornoWms)) {
            $itensEstWms = array_map(fn ($aloc) => [
                'alocacao_id'        => (int)$aloc->id,
                'produto_id'         => (int)$aloc->produto_id,
                'localizacao_codigo' => $aloc->localizacao_codigo,
                'quantidade'         => $aloc->quantidade,
                'origem_leitura'     => $aloc->origem_leitura ?? 'codigo_barras',
                'confirmada_em'      => $aloc->confirmada_em,
                'operacao_tipo'      => $aloc->operacao_tipo ?? 'alocacao',
            ], $estornoWms);
            $porMissaoEstWms = [];
            foreach (array_keys($itensEstWms) as $i) {
                $mid = (int)($estornoWms[$i]->missao_logistica_id ?? 0);
                $porMissaoEstWms[$mid][] = $itensEstWms[$i];
            }
            foreach ($porMissaoEstWms as $missaoId => $itens) {
                $hookWms->estornar($itens, $missaoId);
            }
        }
```

Atualizar o docblock do cron mencionando a 2ª varredura (estorno de cancelados).

- [ ] **Step 5: Rodar e ver passar**

Run: `php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/CronEstornoSmokeTddTest.php`
Expected: `SMOKE OK` (estados saíram de `estorno_pendente`).

- [ ] **Step 6: Commit** (só se autorizado)

```bash
git add crons/wms_alocacao_integracao.php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/CronEstornoSmokeTddTest.php
git commit -m "feat(alocacao-wms): cron empurra estorno de cancelados (2ª varredura)"
```

---

### Task 7: Regressão final + índice opcional

**Files:**
- (Opcional) Create: `componentes/Missoes/Logistica/sinais/AlocacaoWms/Migrations/007_index_estorno.sql`

**Interfaces:**
- Consumes: tudo das tasks anteriores.
- Produces: suíte verde + (opcional) índice parcial p/ a fila de estorno.

- [ ] **Step 1: Rodar a suíte do módulo**

Run (um a um):
```
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Enums/EstadoIntegracaoTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Sondas/WmsMovimentoSondaEstornoTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/RemoverAlocacaoLaboratorioTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Strategies/Operacao/EstornoPayloadTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/HookEstornoTddTest.php
php componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/CronEstornoSmokeTddTest.php
```
Expected: todos verdes (FAIL=0).

- [ ] **Step 2: (Opcional) Índice parcial p/ fila de estorno**

Os índices `idx_wms_movimentos_integ_erp/wms` só cobrem `('pendente','falha')`. Se o volume de cancelados crescer, criar `Migrations/007_index_estorno.sql`:

```sql
CREATE INDEX IF NOT EXISTS idx_wms_movimentos_estorno_erp
  ON wms_movimentos (integracao_erp, block)
  WHERE status = 'cancelada' AND integracao_erp IN ('estorno_pendente','estorno_falha');

CREATE INDEX IF NOT EXISTS idx_wms_movimentos_estorno_wms
  ON wms_movimentos (integracao_wms, block)
  WHERE status = 'cancelada' AND integracao_wms IN ('estorno_pendente','estorno_falha');
```

Decisão: aplicar agora se já houver volume; senão deixar registrado para quando precisar. (Baixa prioridade — não bloqueia o resto.)

- [ ] **Step 3: Commit** (só se autorizado)

```bash
git add componentes/Missoes/Logistica/sinais/AlocacaoWms/Migrations/007_index_estorno.sql
git commit -m "perf(alocacao-wms): índice parcial p/ fila de estorno"
```

---

## Notas de execução

- **S3 (case-sensitivity) e S4 (parse_str)** continuam como caracterização (NOTE) no teste de reprodução — fora de escopo desta rodada.
- **Endpoint externo é stub**: os testes de hook/cron validam o WIRING (a linha sai de `estorno_pendente`), não a resposta do sistema externo. Quando plugar de verdade, `estornado` exigirá `status:ok` real.
- **Permissão**: a remoção segue autorizada pela missão atual (operador legítimo em missão ativa), mesmo cancelando movimentos de outras missões no mesmo endereço — alinhado ao modelo endereço-primeiro.
