# Curadoria Alertas TMS — detectar cancelados em vínculo `problema`

- **Data:** 2026-06-19
- **Branch:** `dev_team`
- **Tipo:** bugfix (detecção)
- **Caso-âncora:** `numero_pedido = 86482` (empresa `b437c567…`), cancelado, preso no romaneio 4894 (tms_id 5473)

## Problema

A modal **Alertas TMS** (curadoria) tem duas seções:
- **A — Bloqueios de Sincronização:** lê `romaneios_tms_log` (eventos `pedido_bloqueado`/`pedido_duplicado`). Informativa, sem ação.
- **B — Pedidos Cancelados Presos em Rota:** lê `romaneios_pedidos`. É a **única com ação "Remover do Romaneio"**.

Um pedido cancelado que está num romaneio ativo **não aparece em nenhuma das duas** — então não há como removê-lo pela curadoria.

## Causa raiz (verificada por dados)

O fluxo de cancelamento do ERP (`sistema/.../lancamentos/pedidos/gerenciar.php:3978-4004`), **na mesma operação** que seta `pedidos.status='cancelado'`:
1. acha `romaneios_pedidos WHERE pedido=… AND status='pendente'`;
2. cria registro em `romaneios_pedidos_problemas` (`tipo_problema='434334'`, "Cancelamento do pedido…");
3. troca `romaneios_pedidos.status` de `pendente` → **`problema`**.

Mas a SEÇÃO B exige `rp.status='pendente'` **E** `p.status='cancelado'`. Como o cancelamento que produz `p.status='cancelado'` é o mesmo que joga o vínculo pra `problema`, as duas condições são **mutuamente exclusivas** no fluxo normal → a seção quase nunca dispara.

A SEÇÃO A também não pega o 86482 porque o pedido entrou no romaneio **antes** de ser cancelado (não houve re-sync bloqueado) — nenhum `pedido_bloqueado` foi logado (queries C/D do diagnóstico vieram vazias).

### Evidência (diagnóstico read-only)

| Query | Resultado relevante |
|---|---|
| A `pedidos numero_pedido=86482` | 2 linhas (numero_pedido **não é único**): `330cc55…` finalizado 2023; **`f2c3c060…` cancelado 2026-06-19** |
| B `romaneios_pedidos numero_pedido=86482` | **`rp_md5=5d94d10d…`, status=`problema`, romaneio 4894 (status `pendente`/ativo), tms_id 5473** |
| C/D `romaneios_tms_log contexto LIKE %86482%` | vazio → nenhum `pedido_bloqueado` registrado |

Os JOINs da curadoria são por `codigoMD5`, então o `numero_pedido` duplicado **não** contamina a detecção (o pedido antigo de 2023 está em romaneio `finalizado` e é corretamente excluído).

## Decisões

1. **Detecção:** surfacing de `rp.status IN ('pendente','problema')` (mantendo `p.status='cancelado'` + romaneio ativo).
2. **Remover:** **só apagar o vínculo** — sem mexer no registro de problema (= comportamento atual do Genesis).
3. **Escopo:** corrigir **as duas** implementações (Genesis + legado).
4. **Legado:** o `remover` do legado já faz limpeza completa (ocorrências/solicitações/itens). **Manter como está** — não alinhar, não estripar. O fix do legado é **só detecção**.

## Mudanças

### Genesis — `componentes/Missoes/Logistica/sinais/Central/Laboratorios/CuradoriaAlertasTmsLab.php`

- **`listarFantasmas()` (≈ linha 253):**
  `WHERE rp.status = 'pendente'` → `WHERE rp.status IN ('pendente','problema')`
- **`empresasTms()` (≈ linha 78), 2º SELECT do UNION:**
  `WHERE rp.status = 'pendente'` → `WHERE rp.status IN ('pendente','problema')`
  (robustez: descobrir a empresa mesmo quando só há vínculo `problema` e nenhum log)
- **`remover()`:** sem mudança (DELETE por `codigoMD5`, guard `p.status='cancelado'` — já funciona em linha `problema`).

### Legado — `sistema/modulos_base/lancamentos/romaneios/CuradoriaAlertas.class.php`

- **`listarPedidosCanceladosEmRomaneios()` (≈ linha 78):**
  `"WHERE status = :s AND empresa_codigoMD5 = :emp"`, `"s=pendente&emp=…"`
  → `"WHERE status IN ('pendente','problema') AND empresa_codigoMD5 = :emp"`, `"emp=…"`
  (lista literal fixa e segura; remove o param `:s`)

### Genesis — `forms/alertas_tms.php` (ajuste de fuso, solicitado durante a implementação)

- **`$fmtData()`:** os timestamps (`cancelado_em` / `data_hora`) são gravados em **UTC** pelo backend, mas a função formatava com `date()` no fuso default (UTC) → mostrava `19h21` em vez de `16h21`. Reescrita com `DateTime`/`DateTimeZone` convertendo **UTC → America/Sao_Paulo**. Como a função é compartilhada, corrige os cards das duas seções (presos em rota + bloqueios). O rótulo "hoje/ontem" passa a ser calculado já no fuso de Brasília (sem erro na virada de dia).

### Sem mudança
- `modals/curadoria_alertas.php` (legado): renderiza `cancelado_em`/`data_hora` crus (sem `$fmtData`) — continua em UTC. Fora de escopo (o usuário usa a modal do Genesis); anotar caso queiram paridade depois.
- Handlers de remover (`remover()` Genesis e `removerPedidoCanceladoDoRomaneio` legado): DELETE não filtra por status → já operam em `problema`.

## Considerações / riscos

- **Precisão mantida:** mesmo com `rp.status='problema'` aberto, o filtro `p.status='cancelado'` garante que só pedidos **cancelados** apareçam — `problema` de outros tipos (avaria, etc.) não entram.
- **Backlog esperado:** podem aparecer cancelados antigos ainda presos em romaneios ativos (sempre estiveram "presos"; agora ficam visíveis). O badge pode subir. Comportamento desejado.
- **Não resolve o lado TMS:** o pedido continua na rota do TMS externo (galpaotms) "confirmado"; não há API de write-back. A curadoria limpa o lado ERP; remover do TMS segue manual. (Fora de escopo.)
- **`numero_pedido` não-único (risco separado):** o guard de `novoRomaneioTMS` (`gerenciar.php:4004`) lê `pedidos WHERE numero_pedido=… AND data_deposito >= '2026-01-01'`. Com números repetidos pode pegar a linha errada. **Fora de escopo deste fix**, anotar como follow-up.

## Verificação (executada)

Rodada a `listarFantasmas` real (com `blocksEmpresa()` + filtro de `block`) com o status novo, empresa `b437c567…`:
- **`dev_goldie_atual`:** `blocksEmpresa=[222,23,22999,22]` → retorna **1 linha = o 86482** (`5d94d10d…`, `problema`, romaneio 4894). ✔️
- **`goldie_atual` (prod):** retorna **4 linhas** = backlog de cancelados presos (7298, 7656, 8870, 9583, jan–abr/2026). Confirma a nota de "backlog esperado".
- O 86482 de 2023 (romaneio `finalizado`) **não** aparece. ✔️
- Fuso: `2026-06-19 19:21:19` (UTC) → `hoje às 16h21`; virada de dia (`00:30 UTC` → `ontem às 21h30`) ok. ✔️

Pendente (manual, em dev): clicar "Remover do Romaneio" e confirmar que o vínculo some + badge decrementa.

## Adendo — socket no remover (reuso do fluxo nativo)

**Problema:** `CuradoriaAlertasTmsLab::remover()` fazia só `DELETE FROM romaneios_pedidos` cru — pulava (1) o socket Scaledrone que reflete num Central aberto, (2) o ajuste de cubagem do veículo e (3) o status do pedido. O remove nativo (`Constelacoes/Logistica/Central/Satelites/Central.php::removerRomaneioPedido`, ~linha 1288) já faz tudo isso (broadcast em `galaxia_logistica_central` via `LogisticaSocketTrait`).

**Decisão:** reusar o fluxo nativo, mantendo o Lab como entrada (rota `labs/apis` + JS atual + guard `cancelado` intactos). Sem mudança de frontend — o socket é global, então a maps.js do operador atual (atrás do modal) e de qualquer outro Central aberto atualizam sozinhos; a remoção do card+badge segue pelo JS atual no retorno `ok`.

**Implementação (`remover()`):**
1. Lê o vínculo + ids numéricos (`rp.id`, `r.id`) por `codigoMD5`.
2. Guard `pedido.status='cancelado'` (mantido).
3. Tenta o nativo: `new SatelitesCentral($this->galaxia, 'removerRomaneioPedido', ['romaneio_id'=>…, 'romaneio_pedido_id'=>…])`.
4. Re-checa se o vínculo sumiu; **se não sumiu** (block do operador ≠ block do romaneio, contexto de Sonda ausente, ou DB divergente) → **fallback** `DELETE` cru (garante a remoção, sem socket).

**Fatos verificados:**
- Sessão existe em `labs/apis`: `system/index.php:25` instancia `Session` → `SessionGalaxia::__construct` chama `session_start()` (linha 38) antes do dispatch, p/ todas as rotas.
- O nativo acha vínculo `problema`: `RomaneiosPedidos` read usa `findById($id)` (RomaneiosPedidos.php:174) — sem filtro de status. Sem no-op silencioso.
- Degradação graciosa: em qualquer cenário (mesma DB+block, block divergente, DB divergente) a remoção acontece; só o socket/cubagem dependem do caminho nativo.

**Trade-off aceito:** o nativo é block-scoped (multi-tenant); o fallback cru é cross-block. Quando cai no fallback, não há socket — aceitável, pois nesse caso o romaneio não está na tela daquele operador.

**Não verificado em runtime:** a execução real do nativo (Sondas com sessão autenticada + publish no Scaledrone) precisa de um clique autenticado no browser — só o código foi escrito + `php -l`.

## Fora de escopo
- Write-back pro TMS externo.
- Correção da não-unicidade de `numero_pedido` no guard.
- Mudança de UI/UX nas modais (o reuso do socket não exigiu front).
- Paridade do remover no legado (a modal legada usa `removerPedidoCanceladoDoRomaneio` → `Pedidos::removerPedidoDoRomaneio`, que já faz limpeza própria; socket nativo não foi adicionado lá).
