# Espelho de Edição de Rota em Solicitações Globais — 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:** Quando o motorista submete uma edição de rota (`submeter_rota` → `edicoes_rota` status `pendente_aprovacao`), espelhar a solicitação em `solicitacoes_globais` para que apareça no painel de Solicitações do `central.php`, no dia efetivo da rota, com aprovação/rejeição sincronizadas nos dois sentidos.

**Architecture:** Todo conhecimento de `solicitacoes_globais` fica isolado num Adapter novo dentro do sinal Edicaorota (`Adapters/Solicitacoes/`), seguindo a convenção V2 `Adapters/<Dominio>/` (igual ao `Adapters/Romaneio/`). O dia efetivo é resolvido pelo adapter Romaneio (`edicoes_rota.callback_referencia_id` → `romaneios_veiculos` → `romaneios.dia_efetivo`). Os 4 Laboratórios core (Submeter/Aprovar/Rejeitar/Cancelar) chamam o adapter em modo best-effort (try/catch + log, nunca quebram o fluxo principal — mesma disciplina do callback existente). A volta (aprovar/rejeitar no painel central aplicando a rota de verdade) usa o ponto de extensão de Strategy do sinal Solicitacoes. O laço é evitado porque `sincronizar()` só atua quando o espelho ainda está `pendente` e escreve direto na Sonda (nunca reinvoca o Laboratório de aprovação global).

**Tech Stack:** PHP 8 (enums backed/const classes), Sondas V2 (`ModelInteligencia`), modelos V1 Romaneio (`Inteligencias`), testes standalone (`*TddTest.php` puro + `*IntegrationTest.php` com `dev_gx_goldie`).

---

## File Structure

| Arquivo | Responsabilidade |
|---|---|
| `Solicitacoes/Enums/SolicitacaoTipo.php` (modificar) | Nova const `EDICAO_ROTA` |
| `Edicaorota/Adapters/Romaneio/Laboratorios/ResolverDiaEfetivoLaboratorio.php` (criar) | `romaneios_veiculos.id` → `romaneios.dia_efetivo` |
| `Edicaorota/Adapters/Solicitacoes/EspelhoSolicitacaoGlobalAdapter.php` (criar) | Único lugar que conhece `SolicitacaoSonda`: `montarPayloadCriacao`, `criar`, `sincronizar` |
| `Solicitacoes/Strategies/EdicaoRotaAprovadaStrategy.php` (criar) | Aprovar no central → aplica rota via `AprovarEdicaoRotaLaboratorio` |
| `Solicitacoes/Strategies/EdicaoRotaRejeitadaStrategy.php` (criar) | Rejeitar no central → rejeita via `RejeitarEdicaoRotaLaboratorio` |
| `Solicitacoes/Laboratorios/AprovarSolicitacaoLaboratorio.php` (modificar) | Registrar strategy de aprovação por tipo |
| `Solicitacoes/Laboratorios/RejeitarSolicitacaoLaboratorio.php` (modificar) | Adicionar dispatch simétrico de rejeição por tipo |
| `Edicaorota/Laboratorios/SubmeterRotaPropostaLaboratorio.php` (modificar) | Criar espelho após save |
| `Edicaorota/Laboratorios/AprovarEdicaoRotaLaboratorio.php` (modificar) | `sincronizar(... 'aprovada' ...)` após save |
| `Edicaorota/Laboratorios/RejeitarEdicaoRotaLaboratorio.php` (modificar) | `sincronizar(... 'rejeitada' ...)` após save |
| `Edicaorota/Laboratorios/CancelarEdicaoRotaLaboratorio.php` (modificar) | `sincronizar(... 'cancelada' ...)` após save |
| `Edicaorota/Testes/Adapters/Solicitacoes/EspelhoSolicitacaoGlobalAdapterTddTest.php` (criar) | TDD puro do payload + guarda |
| `Edicaorota/Testes/Adapters/Romaneio/ResolverDiaEfetivoLaboratorioTddTest.php` (criar) | TDD guarda (id<=0) |
| `Solicitacoes/Testes/Strategies/EdicaoRotaStrategyTddTest.php` (criar) | TDD puro `montarEntrada` |

**Convenções fixadas por código existente:**
- SolicitacaoSonda → tabela `solicitacoes_globais`; NOT NULL: `referencia_tipo`, `referencia_id`, `tipo`, `solicitante_id`. `status` default `pendente`. `data_cadastro` filtrado por `data_cadastro::date = :dia` em `MissoesSolicitacoes::lerSolicitacoes`.
- `SolicitacaoStatus` (sinal): `PENDENTE='pendente'`, `APROVADA='aprovada'`, `REJEITADA='rejeitada'`, `CANCELADA='cancelada'`.
- A view `Central/forms/missao_solicitacoes.php` renderiza linha de `SolicitacaoSonda` sempre como global (header "SOLICITAÇÃO GLOBAL", título = `str_replace('_',' ', tipo)`, corpo = `Referência: {referencia_tipo} #{referencia_id}`; **não** mostra `motivo`). `solicitante_id=0` → `array_filter` descarta → bloco "Solicitante" some. Aceito.
- Modelos Romaneio: `GalaxiaConstelacoes\Romaneios\Crud\Inteligencias\RomaneioVeiculo` (tem `romaneio_id`) e `...\Romaneio` (tem `dia_efetivo` via `__get`). `findById($id)` pode devolver array → normalizar `is_array(...) ? ($x[0] ?? null) : $x`.
- Edicaorota labs core recebem `array $entrada` no construtor; `AprovarEdicaoRotaLaboratorio` espera `edicao_id`, `respondido_por`, `resposta`; `RejeitarEdicaoRotaLaboratorio` espera `edicao_id`, `respondido_por`, `motivo_rejeicao`.
- Rodar teste puro: `php <caminho-do-TddTest>`. Integration: exige `dev_gx_goldie` + bootstrap de sessão (não cobertos aqui como gate obrigatório de CI).

---

### Task 1: Const EDICAO_ROTA

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Solicitacoes/Enums/SolicitacaoTipo.php`

- [ ] **Step 1: Adicionar a const**

Em `class SolicitacaoTipo`, após `public const ACESSO_PESAGEM = 'ACESSO_PESAGEM';` adicionar:

```php
    public const EDICAO_ROTA = 'EDICAO_ROTA';
```

- [ ] **Step 2: Lint**

Run: `php -l componentes/Missoes/Logistica/sinais/Solicitacoes/Enums/SolicitacaoTipo.php`
Expected: `No syntax errors detected`

- [ ] **Step 3: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Solicitacoes/Enums/SolicitacaoTipo.php
git commit -m "feat(solicitacoes): add EDICAO_ROTA tipo"
```

---

### Task 2: ResolverDiaEfetivoLaboratorio (Adapter Romaneio)

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Edicaorota/Adapters/Romaneio/Laboratorios/ResolverDiaEfetivoLaboratorio.php`
- Test: `componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Adapters/Romaneio/ResolverDiaEfetivoLaboratorioTddTest.php`

- [ ] **Step 1: Escrever teste de guarda (puro, sem DB)**

```php
<?php
// ResolverDiaEfetivoLaboratorioTddTest.php
require_once __DIR__ . '/../../../Adapters/Romaneio/Laboratorios/ResolverDiaEfetivoLaboratorio.php';

use GalaxiaMissoes\Logistica\sinais\Edicaorota\Adapters\Romaneio\Laboratorios\ResolverDiaEfetivoLaboratorio;

$falhas = 0;
$assert = function (bool $c, string $m) use (&$falhas) {
    echo ($c ? "✅ [PASS] " : "❌ [FAIL] ") . $m . "\n";
    if (!$c) $falhas++;
};

// id inválido => null, sem tocar DB
$assert(ResolverDiaEfetivoLaboratorio::resolver(0) === null, 'veiculoId 0 => null');
$assert(ResolverDiaEfetivoLaboratorio::resolver(-5) === null, 'veiculoId negativo => null');

echo $falhas === 0 ? "\nTODOS OK\n" : "\n{$falhas} FALHAS\n";
exit($falhas === 0 ? 0 : 1);
```

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

Run: `php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Adapters/Romaneio/ResolverDiaEfetivoLaboratorioTddTest.php`
Expected: erro fatal "class not found".

- [ ] **Step 3: Implementar**

```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\Edicaorota\Adapters\Romaneio\Laboratorios;

use GalaxiaConstelacoes\Romaneios\Crud\Inteligencias\Romaneio;
use GalaxiaConstelacoes\Romaneios\Crud\Inteligencias\RomaneioVeiculo;

/**
 * Resolve o dia_efetivo do romaneio a partir do romaneios_veiculos.id
 * (callback_referencia_id de uma edicoes_rota com callback_modulo='romaneio_veiculo').
 *
 * Retorna 'Y-m-d' ou null se não conseguir resolver. Nunca lança.
 */
class ResolverDiaEfetivoLaboratorio
{
    public static function resolver(int $veiculoId): ?string
    {
        if ($veiculoId <= 0) {
            return null;
        }

        try {
            $mv = (new RomaneioVeiculo())->findById($veiculoId);
            $mv = is_array($mv) ? ($mv[0] ?? null) : $mv;
            if (empty($mv) || empty($mv->romaneio_id)) {
                return null;
            }

            $rom = (new Romaneio())->findById((int) $mv->romaneio_id);
            $rom = is_array($rom) ? ($rom[0] ?? null) : $rom;
            if (empty($rom) || empty($rom->dia_efetivo)) {
                return null;
            }

            $ts = strtotime((string) $rom->dia_efetivo);
            return $ts ? date('Y-m-d', $ts) : null;
        } catch (\Throwable $e) {
            error_log('[ResolverDiaEfetivoLaboratorio] ' . $e->getMessage());
            return null;
        }
    }
}
```

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

Run: `php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Adapters/Romaneio/ResolverDiaEfetivoLaboratorioTddTest.php`
Expected: `TODOS OK`

- [ ] **Step 5: Lint + commit**

```bash
php -l componentes/Missoes/Logistica/sinais/Edicaorota/Adapters/Romaneio/Laboratorios/ResolverDiaEfetivoLaboratorio.php
git add componentes/Missoes/Logistica/sinais/Edicaorota/Adapters/Romaneio/Laboratorios/ResolverDiaEfetivoLaboratorio.php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Adapters/Romaneio/ResolverDiaEfetivoLaboratorioTddTest.php
git commit -m "feat(edicaorota): resolver dia_efetivo via adapter romaneio"
```

---

### Task 3: EspelhoSolicitacaoGlobalAdapter

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Edicaorota/Adapters/Solicitacoes/EspelhoSolicitacaoGlobalAdapter.php`
- Test: `componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Adapters/Solicitacoes/EspelhoSolicitacaoGlobalAdapterTddTest.php`

- [ ] **Step 1: Teste puro de `montarPayloadCriacao`**

```php
<?php
require_once __DIR__ . '/../../../Adapters/Solicitacoes/EspelhoSolicitacaoGlobalAdapter.php';

use GalaxiaMissoes\Logistica\sinais\Edicaorota\Adapters\Solicitacoes\EspelhoSolicitacaoGlobalAdapter;

$falhas = 0;
$assert = function (bool $c, string $m) use (&$falhas) {
    echo ($c ? "✅ [PASS] " : "❌ [FAIL] ") . $m . "\n";
    if (!$c) $falhas++;
};

$edicao = (object) ['id' => 42, 'identificador_motorista' => 'João'];
$p = EspelhoSolicitacaoGlobalAdapter::montarPayloadCriacao($edicao, '2026-05-05');

$assert($p['referencia_tipo'] === 'edicao_rota', 'referencia_tipo');
$assert($p['referencia_id'] === 42, 'referencia_id = edicao.id');
$assert($p['tipo'] === 'EDICAO_ROTA', 'tipo EDICAO_ROTA');
$assert($p['solicitante_id'] === 0, 'solicitante_id 0 (motorista não é user)');
$assert($p['status'] === 'pendente', 'status pendente');
$assert($p['data_cadastro'] === '2026-05-05 00:00:00', 'data_cadastro = dia efetivo 00:00:00');
$assert($p['block'] === 1, 'block 1');
$assert(strpos($p['motivo'], '#42') !== false && strpos($p['motivo'], 'João') !== false, 'motivo cita edição e motorista');

echo $falhas === 0 ? "\nTODOS OK\n" : "\n{$falhas} FALHAS\n";
exit($falhas === 0 ? 0 : 1);
```

- [ ] **Step 2: Rodar — falha (classe não existe)**

Run: `php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Adapters/Solicitacoes/EspelhoSolicitacaoGlobalAdapterTddTest.php`
Expected: "class not found".

- [ ] **Step 3: Implementar**

```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\Edicaorota\Adapters\Solicitacoes;

use GalaxiaMissoes\Logistica\sinais\Solicitacoes\Sondas\SolicitacaoSonda;

/**
 * Único ponto do sinal Edicaorota que conhece `solicitacoes_globais`.
 * Espelha uma edição de rota como solicitação global e mantém o status
 * sincronizado. Idempotente; nunca lança (best-effort).
 */
class EspelhoSolicitacaoGlobalAdapter
{
    private const REFERENCIA_TIPO = 'edicao_rota';

    /** Payload puro (testável sem DB). */
    public static function montarPayloadCriacao(object $edicao, string $diaEfetivo): array
    {
        $id   = (int) ($edicao->id ?? 0);
        $mot  = trim((string) ($edicao->identificador_motorista ?? ''));
        $motivo = "Motorista propôs nova ordem de entrega (edição de rota #{$id})."
            . ($mot !== '' ? " Motorista: {$mot}." : '');

        return [
            'referencia_tipo' => self::REFERENCIA_TIPO,
            'referencia_id'   => $id,
            'tipo'            => 'EDICAO_ROTA',
            'solicitante_id'  => 0,
            'motivo'          => $motivo,
            'status'          => 'pendente',
            'data_cadastro'   => $diaEfetivo . ' 00:00:00',
            'block'           => 1,
        ];
    }

    /** Cria o espelho se ainda não existir. Best-effort. */
    public static function criar(object $edicao, string $diaEfetivo): void
    {
        try {
            if (self::buscarEspelho((int) ($edicao->id ?? 0)) !== null) {
                return; // idempotente
            }
            $sonda = new SolicitacaoSonda();
            $sonda->make(self::montarPayloadCriacao($edicao, $diaEfetivo));
            if (!$sonda->save()) {
                error_log('[EspelhoSolicitacaoGlobalAdapter] save() falhou ao criar espelho');
            }
        } catch (\Throwable $e) {
            error_log('[EspelhoSolicitacaoGlobalAdapter::criar] ' . $e->getMessage());
        }
    }

    /**
     * Sincroniza o espelho para um status terminal — SOMENTE se ainda
     * estiver 'pendente' (guarda anti-laço; escrita direta na Sonda).
     * $statusGlobal: 'aprovada' | 'rejeitada' | 'cancelada'.
     */
    public static function sincronizar(int $edicaoId, string $statusGlobal, ?int $respondidoPor, ?string $resposta): void
    {
        try {
            $espelho = self::buscarEspelho($edicaoId);
            if ($espelho === null || ($espelho->status ?? null) !== 'pendente') {
                return; // já resolvido ou inexistente — no-op (evita laço)
            }
            $sonda = new SolicitacaoSonda();
            $sonda->make([
                'id'              => (int) $espelho->id,
                'status'          => $statusGlobal,
                'respondido_por'  => $respondidoPor,
                'resposta_gestor' => $resposta,
                'respondido_em'   => date('Y-m-d H:i:s'),
            ]);
            if (!$sonda->save()) {
                error_log('[EspelhoSolicitacaoGlobalAdapter] save() falhou ao sincronizar espelho');
            }
        } catch (\Throwable $e) {
            error_log('[EspelhoSolicitacaoGlobalAdapter::sincronizar] ' . $e->getMessage());
        }
    }

    private static function buscarEspelho(int $edicaoId): ?object
    {
        if ($edicaoId <= 0) {
            return null;
        }
        $r = (new SolicitacaoSonda())
            ->find(
                'referencia_tipo = :rt AND referencia_id = :ri',
                'rt=' . self::REFERENCIA_TIPO . "&ri={$edicaoId}"
            )
            ->fetch(true);
        if (empty($r)) {
            return null;
        }
        return is_array($r) ? ($r[0] ?? null) : $r;
    }
}
```

- [ ] **Step 4: Rodar — passa**

Run: `php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Adapters/Solicitacoes/EspelhoSolicitacaoGlobalAdapterTddTest.php`
Expected: `TODOS OK`

- [ ] **Step 5: Lint + commit**

```bash
php -l componentes/Missoes/Logistica/sinais/Edicaorota/Adapters/Solicitacoes/EspelhoSolicitacaoGlobalAdapter.php
git add componentes/Missoes/Logistica/sinais/Edicaorota/Adapters/Solicitacoes/EspelhoSolicitacaoGlobalAdapter.php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Adapters/Solicitacoes/EspelhoSolicitacaoGlobalAdapterTddTest.php
git commit -m "feat(edicaorota): adapter espelho solicitacao global"
```

---

### Task 4: Strategies de volta (central aplica a rota) + sync simétrico

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Solicitacoes/Strategies/EdicaoRotaAprovadaStrategy.php`
- Create: `componentes/Missoes/Logistica/sinais/Solicitacoes/Strategies/EdicaoRotaRejeitadaStrategy.php`
- Modify: `componentes/Missoes/Logistica/sinais/Solicitacoes/Laboratorios/AprovarSolicitacaoLaboratorio.php:106-109`
- Modify: `componentes/Missoes/Logistica/sinais/Solicitacoes/Laboratorios/RejeitarSolicitacaoLaboratorio.php` (após o save de rejeição, antes do `return success`)
- Test: `componentes/Missoes/Logistica/sinais/Solicitacoes/Testes/Strategies/EdicaoRotaStrategyTddTest.php`

- [ ] **Step 1: Teste puro de `montarEntrada`**

```php
<?php
require_once __DIR__ . '/../../Strategies/EdicaoRotaAprovadaStrategy.php';
require_once __DIR__ . '/../../Strategies/EdicaoRotaRejeitadaStrategy.php';

use GalaxiaMissoes\Logistica\sinais\Solicitacoes\Strategies\EdicaoRotaAprovadaStrategy;
use GalaxiaMissoes\Logistica\sinais\Solicitacoes\Strategies\EdicaoRotaRejeitadaStrategy;

$falhas = 0;
$assert = function (bool $c, string $m) use (&$falhas) {
    echo ($c ? "✅ [PASS] " : "❌ [FAIL] ") . $m . "\n";
    if (!$c) $falhas++;
};

$sol = (object) ['referencia_id' => 42, 'respondido_por' => 7, 'resposta_gestor' => 'ok'];

$ea = EdicaoRotaAprovadaStrategy::montarEntrada($sol);
$assert($ea['edicao_id'] === 42 && $ea['respondido_por'] === 7 && $ea['resposta'] === 'ok', 'aprovar => edicao_id/respondido_por/resposta');

$er = EdicaoRotaRejeitadaStrategy::montarEntrada($sol);
$assert($er['edicao_id'] === 42 && $er['respondido_por'] === 7 && $er['motivo_rejeicao'] === 'ok', 'rejeitar => edicao_id/respondido_por/motivo_rejeicao');

echo $falhas === 0 ? "\nTODOS OK\n" : "\n{$falhas} FALHAS\n";
exit($falhas === 0 ? 0 : 1);
```

- [ ] **Step 2: Rodar — falha (classes não existem)**

Run: `php componentes/Missoes/Logistica/sinais/Solicitacoes/Testes/Strategies/EdicaoRotaStrategyTddTest.php`
Expected: "class not found".

- [ ] **Step 3: Implementar `EdicaoRotaAprovadaStrategy`**

```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\Solicitacoes\Strategies;

use GalaxiaMissoes\Logistica\sinais\Edicaorota\Laboratorios\AprovarEdicaoRotaLaboratorio;

/**
 * Side-effect de aprovação no painel central: aplica a rota de verdade
 * delegando ao Laboratório do sinal Edicaorota (que dispara o callback
 * Romaneio e grava sequencia_roteirizacao).
 */
class EdicaoRotaAprovadaStrategy
{
    public static function montarEntrada(object $solicitacao): array
    {
        return [
            'edicao_id'      => (int) ($solicitacao->referencia_id ?? 0),
            'respondido_por' => $solicitacao->respondido_por ?? null,
            'resposta'       => $solicitacao->resposta_gestor ?? null,
        ];
    }

    public function executar(object $solicitacao): bool
    {
        try {
            $entrada = self::montarEntrada($solicitacao);
            if ($entrada['edicao_id'] <= 0) {
                error_log('[EdicaoRotaAprovadaStrategy] referencia_id inválido');
                return false;
            }
            $resp = (new AprovarEdicaoRotaLaboratorio($entrada))->response;
            if (($resp['notification_class'] ?? '') === 'error') {
                error_log('[EdicaoRotaAprovadaStrategy] aprovação Edicaorota falhou');
                return false;
            }
            return true;
        } catch (\Throwable $e) {
            error_log('[EdicaoRotaAprovadaStrategy] ' . $e->getMessage());
            return false;
        }
    }
}
```

- [ ] **Step 4: Implementar `EdicaoRotaRejeitadaStrategy`**

```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\Solicitacoes\Strategies;

use GalaxiaMissoes\Logistica\sinais\Edicaorota\Laboratorios\RejeitarEdicaoRotaLaboratorio;

/**
 * Side-effect de rejeição no painel central: rejeita a edição de rota
 * correspondente no sinal Edicaorota.
 */
class EdicaoRotaRejeitadaStrategy
{
    public static function montarEntrada(object $solicitacao): array
    {
        $motivo = $solicitacao->resposta_gestor ?? null;
        return [
            'edicao_id'       => (int) ($solicitacao->referencia_id ?? 0),
            'respondido_por'  => $solicitacao->respondido_por ?? null,
            'motivo_rejeicao' => ($motivo !== null && $motivo !== '') ? $motivo : 'Rejeitada pelo gestor.',
        ];
    }

    public function executar(object $solicitacao): bool
    {
        try {
            $entrada = self::montarEntrada($solicitacao);
            if ($entrada['edicao_id'] <= 0) {
                error_log('[EdicaoRotaRejeitadaStrategy] referencia_id inválido');
                return false;
            }
            $resp = (new RejeitarEdicaoRotaLaboratorio($entrada))->response;
            if (($resp['notification_class'] ?? '') === 'error') {
                error_log('[EdicaoRotaRejeitadaStrategy] rejeição Edicaorota falhou');
                return false;
            }
            return true;
        } catch (\Throwable $e) {
            error_log('[EdicaoRotaRejeitadaStrategy] ' . $e->getMessage());
            return false;
        }
    }
}
```

- [ ] **Step 5: Registrar strategy de aprovação**

Em `AprovarSolicitacaoLaboratorio.php`, no array `$mapaStrategies` (linhas ~106-109), adicionar a entrada:

```php
        $mapaStrategies = [
            SolicitacaoTipo::ACESSO_RETIRA  => \GalaxiaMissoes\Logistica\sinais\Solicitacoes\Strategies\AcessoRetiraAprovadoStrategy::class,
            SolicitacaoTipo::ACESSO_PESAGEM => \GalaxiaMissoes\Logistica\sinais\Solicitacoes\Strategies\AcessoPesagemAprovadoStrategy::class,
            SolicitacaoTipo::EDICAO_ROTA    => \GalaxiaMissoes\Logistica\sinais\Solicitacoes\Strategies\EdicaoRotaAprovadaStrategy::class,
        ];
```

- [ ] **Step 6: Dispatch simétrico de rejeição**

Em `RejeitarSolicitacaoLaboratorio.php`, logo após `$sondaHistorico->make([...])->save();` e antes de `return $this->message->success('Solicitação rejeitada com sucesso.')->render();`, inserir:

```php
            if (($solicitacaoEncontrada->tipo ?? null) === SolicitacaoTipo::EDICAO_ROTA) {
                (new \GalaxiaMissoes\Logistica\sinais\Solicitacoes\Strategies\EdicaoRotaRejeitadaStrategy())
                    ->executar($solicitacaoEncontrada);
            }
```

E adicionar no topo, junto aos outros `use`:

```php
use GalaxiaMissoes\Logistica\sinais\Solicitacoes\Enums\SolicitacaoTipo;
```

(`AprovarSolicitacaoLaboratorio` já importa `SolicitacaoTipo`.)

- [ ] **Step 7: Rodar teste — passa**

Run: `php componentes/Missoes/Logistica/sinais/Solicitacoes/Testes/Strategies/EdicaoRotaStrategyTddTest.php`
Expected: `TODOS OK`

- [ ] **Step 8: Lint + commit**

```bash
php -l componentes/Missoes/Logistica/sinais/Solicitacoes/Strategies/EdicaoRotaAprovadaStrategy.php
php -l componentes/Missoes/Logistica/sinais/Solicitacoes/Strategies/EdicaoRotaRejeitadaStrategy.php
php -l componentes/Missoes/Logistica/sinais/Solicitacoes/Laboratorios/AprovarSolicitacaoLaboratorio.php
php -l componentes/Missoes/Logistica/sinais/Solicitacoes/Laboratorios/RejeitarSolicitacaoLaboratorio.php
git add componentes/Missoes/Logistica/sinais/Solicitacoes/Strategies/ componentes/Missoes/Logistica/sinais/Solicitacoes/Laboratorios/AprovarSolicitacaoLaboratorio.php componentes/Missoes/Logistica/sinais/Solicitacoes/Laboratorios/RejeitarSolicitacaoLaboratorio.php componentes/Missoes/Logistica/sinais/Solicitacoes/Testes/Strategies/EdicaoRotaStrategyTddTest.php
git commit -m "feat(solicitacoes): strategies edicao-rota aprovar/rejeitar bidirecional"
```

---

### Task 5: Hook nos 4 Laboratórios core do Edicaorota

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Edicaorota/Laboratorios/SubmeterRotaPropostaLaboratorio.php:63-67`
- Modify: `componentes/Missoes/Logistica/sinais/Edicaorota/Laboratorios/AprovarEdicaoRotaLaboratorio.php:85-87`
- Modify: `componentes/Missoes/Logistica/sinais/Edicaorota/Laboratorios/RejeitarEdicaoRotaLaboratorio.php:80-82`
- Modify: `componentes/Missoes/Logistica/sinais/Edicaorota/Laboratorios/CancelarEdicaoRotaLaboratorio.php:73-78`

- [ ] **Step 1: Submeter — criar espelho após save**

Em `SubmeterRotaPropostaLaboratorio::executar`, substituir o bloco final:

```php
        if (!$sonda->save()) {
            return $this->message->error("Falha ao salvar rota proposta.")->render();
        }

        return $this->message->success("Rota enviada para aprovação do gestor.")->render();
```

por:

```php
        if (!$sonda->save()) {
            return $this->message->error("Falha ao salvar rota proposta.")->render();
        }

        try {
            $edicaoCompleta = $sonda->findByToken($entrada['token'] ?? '');
            $edicaoCompleta = is_array($edicaoCompleta) ? ($edicaoCompleta[0] ?? null) : $edicaoCompleta;
            if (!empty($edicaoCompleta)) {
                $dia = null;
                if (($edicaoCompleta->callback_modulo ?? '') === 'romaneio_veiculo') {
                    $dia = \GalaxiaMissoes\Logistica\sinais\Edicaorota\Adapters\Romaneio\Laboratorios\ResolverDiaEfetivoLaboratorio::resolver(
                        (int) ($edicaoCompleta->callback_referencia_id ?? 0)
                    );
                }
                $dia = $dia ?: date('Y-m-d');
                \GalaxiaMissoes\Logistica\sinais\Edicaorota\Adapters\Solicitacoes\EspelhoSolicitacaoGlobalAdapter::criar($edicaoCompleta, $dia);
            }
        } catch (\Throwable $e) {
            error_log('[SubmeterRotaProposta] espelho falhou: ' . $e->getMessage());
        }

        return $this->message->success("Rota enviada para aprovação do gestor.")->render();
```

- [ ] **Step 2: Aprovar — sincronizar espelho**

Em `AprovarEdicaoRotaLaboratorio::executar`, logo após o bloco `try { ... callback ... } catch (...) { ... }` e antes de `return $this->message->success("Edição aprovada e aplicada.")->render();`, inserir:

```php
            \GalaxiaMissoes\Logistica\sinais\Edicaorota\Adapters\Solicitacoes\EspelhoSolicitacaoGlobalAdapter::sincronizar(
                (int) $edicao->id,
                'aprovada',
                (int) $entrada['respondido_por'],
                $entrada['resposta'] ?? null
            );
```

- [ ] **Step 3: Rejeitar — sincronizar espelho**

Em `RejeitarEdicaoRotaLaboratorio::executar`, antes de `return $this->message->success("Edição rejeitada.")->render();`, inserir:

```php
            \GalaxiaMissoes\Logistica\sinais\Edicaorota\Adapters\Solicitacoes\EspelhoSolicitacaoGlobalAdapter::sincronizar(
                (int) $edicao->id,
                'rejeitada',
                (int) $entrada['respondido_por'],
                $entrada['motivo_rejeicao'] ?? null
            );
```

- [ ] **Step 4: Cancelar — sincronizar espelho**

Em `CancelarEdicaoRotaLaboratorio::executar`, após `if (!$sonda->save()) {...}` e antes de montar `$resposta = $this->message->success("Edição cancelada.")...`, inserir:

```php
            \GalaxiaMissoes\Logistica\sinais\Edicaorota\Adapters\Solicitacoes\EspelhoSolicitacaoGlobalAdapter::sincronizar(
                (int) $edicao->id,
                'cancelada',
                isset($entrada['cancelado_por']) ? (int) $entrada['cancelado_por'] : null,
                $entrada['motivo_cancelamento'] ?? null
            );
```

- [ ] **Step 5: Lint dos 4**

```bash
php -l componentes/Missoes/Logistica/sinais/Edicaorota/Laboratorios/SubmeterRotaPropostaLaboratorio.php
php -l componentes/Missoes/Logistica/sinais/Edicaorota/Laboratorios/AprovarEdicaoRotaLaboratorio.php
php -l componentes/Missoes/Logistica/sinais/Edicaorota/Laboratorios/RejeitarEdicaoRotaLaboratorio.php
php -l componentes/Missoes/Logistica/sinais/Edicaorota/Laboratorios/CancelarEdicaoRotaLaboratorio.php
```
Expected: `No syntax errors detected` nos quatro.

- [ ] **Step 6: Rerodar TODOS os TddTests existentes do Edicaorota (não-regressão)**

```bash
for f in componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Laboratorios/*TddTest.php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Enums/*TddTest.php; do echo "== $f =="; php "$f"; done
```
Expected: nenhum `[FAIL]` novo.

- [ ] **Step 7: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Edicaorota/Laboratorios/
git commit -m "feat(edicaorota): espelhar/sincronizar solicitacao global nos 4 labs"
```

---

### Task 6: Verificação fim-a-fim

**Files:** nenhum (verificação).

- [ ] **Step 1: Rodar toda a suíte TDD nova + existente**

```bash
php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Adapters/Romaneio/ResolverDiaEfetivoLaboratorioTddTest.php
php componentes/Missoes/Logistica/sinais/Edicaorota/Testes/Adapters/Solicitacoes/EspelhoSolicitacaoGlobalAdapterTddTest.php
php componentes/Missoes/Logistica/sinais/Solicitacoes/Testes/Strategies/EdicaoRotaStrategyTddTest.php
```
Expected: `TODOS OK` nos três.

- [ ] **Step 2: Conferência manual anti-laço (revisão de código, não execução)**

Confirmar lendo o código: `sincronizar()` só escreve quando espelho `status === 'pendente'`; fluxo central (`AprovarSolicitacaoLaboratorio` guarda `PENDENTE` → strategy → `AprovarEdicaoRotaLaboratorio` → `sincronizar` acha espelho já `aprovada` → no-op). Sem recursão.

- [ ] **Step 3: (Opcional, requer `dev_gx_goldie`) smoke integration**

Criar `edicoes_rota` de teste com `callback_modulo='romaneio_veiculo'`, rodar submeter→conferir linha em `solicitacoes_globais` (`referencia_tipo='edicao_rota'`, `data_cadastro::date` = dia_efetivo do romaneio, `status='pendente'`); aprovar via painel central→conferir `edicoes_rota.status='aprovada'` e `sequencia_roteirizacao` aplicada e espelho `aprovada`.

- [ ] **Step 4: Marcar checkboxes do plano e fechar tasks**

---

## Self-Review

**Spec coverage:**
- "Espelhar em SolicitacaoSonda no submeter_rota" → Task 3 + Task 5 Step 1. ✔
- "Aparece no dia efetivo da rota" → Task 2 (resolver) + `data_cadastro = dia 00:00:00` (Task 3) consumido em Task 5 Step 1. ✔
- "Aprovar no central aplica a rota" → Task 4 strategy de aprovação registrada no map de `AprovarSolicitacaoLaboratorio`. ✔
- "Sincronização bidirecional / rejeitar também" → Task 4 (reject dispatch simétrico) + Task 5 (sincronizar em Aprovar/Rejeitar/Cancelar). ✔
- "Sem tocar V1 (`MissaoLogistica`/`MissoesSolicitacoes`)" → nenhuma modificação na Constelação V1; só sinal V2 Solicitacoes (que já tem o padrão Strategy) e o sinal Edicaorota. ✔
- Anti-laço → guarda `pendente` em `sincronizar` (Task 3) + Self-Review Task 6 Step 2. ✔

**Placeholder scan:** sem TBD/TODO; todo passo de código tem código completo. ✔

**Type consistency:** `EspelhoSolicitacaoGlobalAdapter::criar/sincronizar/montarPayloadCriacao/buscarEspelho`, `ResolverDiaEfetivoLaboratorio::resolver`, `EdicaoRota{Aprovada,Rejeitada}Strategy::montarEntrada/executar`, `SolicitacaoTipo::EDICAO_ROTA` — assinaturas idênticas entre tasks de definição e de uso. ✔

**Limitação conhecida documentada:** a view global do central mostra só tipo + "Referência: edicao_rota #id" (não mostra `motivo`/solicitante porque `solicitante_id=0`); aceito por decisão (não tocar a view V1).
