💡 Ctrl+P → Salvar como PDF

# Análise Sênior · Módulo Agenda Estado pós-bateria de testes C1–C13 · 2026-05-20 · Sistema AgênciaPSI

Files
142
Arquivos no escopo agenda
LOC
15.6k
Linhas em hotspots-chave
Bugs/dia
14
Corrigidos em 20/05
Commits/dia
15
Working tree clean

## 0Sumário Executivo

O módulo de agenda do AgênciaPSI atravessou uma rodada estrutural de validação via 13 cenários do documento vivo `src/docs/agenda-compromisso-financeiro-cenarios.html`. A bateria expôs 14 bugs reais em apenas um dia — todos corrigidos — e revelou um padrão recorrente: regressões silenciosas em fluxos paralelos, particularmente em interações com `billing_contracts` e `financial_records`.

A engenharia do módulo é funcionalmente robusta — os fluxos críticos (status change, pacote saldo, pacote upfront, multa, antecipação, edit imutável) operam corretamente após os fixes. Mas a base de código carrega sintomas que devem ser endereçados antes da replicação para Rail e Clínica, sob risco de duplicação de dívida.

Verdict resumido: arquitetura sólida em conceito, dívida técnica acumulada em execução. Refatoração de 3 hotspots e adoção de tipagem forte resolveriam ≥60% dos bugs futuros antes de virarem produção. A replicação Rail/Clínica é o ponto natural de inflexão para consolidar.

### Top 5 achados

1. 3 hotspots ultrapassam 2,8k LOC cada — `AgendaEventDialog.vue` (6.070), `MelissaLayout.vue` (4.331), `AgendaTerapeutaPage.vue` (3.567). Refator sustentado é inviável neste estado. P0
2. Lógica de status change triplicada entre Melissa, Rail e Clínica. Bugs corrigidos hoje em Melissa ainda persistem nas outras duas implementações. P0
3. Gotcha `billing_contracts.updated_at` — coluna inexistente. UPDATEs falhavam silenciosamente em `Promise.allSettled`. Patch tipo "trigger SQL" eliminaria a categoria inteira. P1
4. Snapshot stale em popover — `eventoSelecionado.value` é cópia, não referência reativa. Padrão repetido em outros componentes do Melissa. Ergonomia ruim para mutações otimistas. P1
5. Audit trail acumulando ruído — ciclos antecipar/revogar/realizar geram records cancelled que poluem a view `/financeiro` sem filtro. Falta UI dedicada de auditoria. P2

## 1Estado atual

### 1.1 Distribuição de complexidade (hotspots)

Arquivo | LOC | Tipo | Observação
--- | --- | --- | ---
`AgendaEventDialog.vue` | 6.070 | Componente Vue monolítico | ≥7 responsabilidades distintas: cadastro, recorrência, billing, multa, pacote, lock, preview
`MelissaLayout.vue` | 4.331 | Layout container | Container + ~30 handlers + 8 dialogs montados; deveria ser shell
`AgendaTerapeutaPage.vue` | 3.567 | Página Rail (legacy) | Implementação paralela com lógica duplicada
`useMelissaAgenda.js` | 2.863 | Composable monolítico | Bulk-load, normalize, handlers, status change, decisions, applies — tudo aqui
`AgendaClinicaPage.vue` | 2.811 | Página Clínica (legacy) | 3ª implementação paralela; receberá os mesmos bugs
`MelissaEventoPanel.vue` | 1.010 | Popover | Aceitável, mas concentra lógica de derivação (paymentLabel, contractInfo, ações)
`useAgendaEventLifecycle.js` | 685 | Composable de lifecycle | Carregamento, edit, delete; saudável em tamanho
`AgendaStatusChangeConfirmDialog.vue` | 671 | Dialog dedicado | Recém-ampliado com bloco reverse (Fase 5); ainda dentro do aceitável

Total nos hotspots acima: ~22k LOC. Em escala de manutenção, qualquer arquivo >1.000 LOC é candidato a refator; >2.500 LOC é débito crítico — toda navegação demanda Ctrl+F.

### 1.2 Estrutura de pastas (recap)

    src/
    ├── features/agenda/
    │   ├── components/        (15 .vue — DialogV2, Toolbar, Calendar, etc)
    │   ├── composables/       (20 .js — lifecycle, events, services, etc)
    │   └── pages/             (5 .vue — Terapeuta, Clínica, Recorrencias, etc)
    ├── layout/melissa/
    │   ├── composables/       (useMelissaAgenda 2.863 LOC)
    │   ├── MelissaLayout.vue  (shell + handlers)
    │   ├── MelissaAgenda.vue  (FullCalendar wrapper)
    │   ├── MelissaEventoPanel (popover)
    │   └── ...
    └── components/agenda/     (AgendaEventoFinanceiroPanel, etc)

### 1.3 Cobertura de testes

`src/features/agenda/composables/__tests__/` tem 7 specs Vitest cobrindo composables principais (events, lifecycle, composer, picker, recurrence). Não há testes E2E. Toda validação de fluxo financeiro (status change, pacote, antecipar) depende de bateria manual contra o documento vivo HTML.

Métrica de risco: 14 bugs encontrados num único dia de teste manual num único persona (Melissa). Extrapolando para Rail/Clínica sem refator antes, esperar 25–35 bugs equivalentes na replicação.

## 2Pontos fortes

Análise reconhece méritos antes de criticar. O que está bem feito:

### 2.1 Cobertura de cenários documentada

O arquivo `src/docs/agenda-compromisso-financeiro-cenarios.html` é uma boa prática rara: doc vivo que descreve cada cenário com mockup interativo + critério de teste. Funciona como spec executável informal. O addendum de C10 que adicionamos hoje preservou o histórico de divergências. Manter

### 2.2 Audit trail em `financial_records.notes`

Todas as transições de cancelamento, reversão e revogação registram entrada cronológica em `notes` com formato `[YYYY-MM-DD] motivo`. Permite reconstruir o histórico financeiro de qualquer sessão. Estrutura simples mas efetiva — não criou tabela de auditoria paralela. Manter

### 2.3 Reverse transition trava (recém-implementada)

O dialog `AgendaStatusChangeConfirmDialog` agora cobre 4 contextos distintos: realizada (avulsa/pacote), faltou/cancelado (com multa+saldo), realizada com paid pré-existente (antecipação), e reversão para agendado (com cancelar records + devolver saldo). É o ponto único de decisão para mudanças com impacto financeiro. Manter

### 2.4 Bubble `@cobranca-atualizada`

Sub-componentes (panel financeiro, dialog) emitem evento que sobe até `MelissaLayout` e dispara `M.refetch()`. Padrão limpo para propagar updates de pagamento sem acoplar pais e filhos via store global. Manter — replicar em Rail/Clínica

### 2.5 Lock em sessão encerrada

Sessões em `cancelado`/`faltou` agora bloqueiam "Editar sessão" + transições de status (exceto "Agendada" como caminho explícito de recuperação) + label e badge cientes do estado. Evita operações financeiras inadvertidas em sessões que não aconteceram. Manter

### 2.6 Schema com constraints adequadas

`financial_exceptions_charge_chk` restringe valores possíveis de `charge_mode`. `financial_records.status_check` enumera estados válidos. Constraints de check ao invés de validação apenas em código — defesa em profundidade. Manter

### 2.7 Quick-create inline

Em vez de navegar para outro form quando falta uma dep (serviço, convênio, paciente), abre quick-create por cima. Preserva contexto do usuário. Padrão consistente. Manter — padrão de design

### 2.8 Convenção de variantes visuais

Cores semânticas consistentes — `--ok` (verde realizada), `--warn` (amarelo falta), `--danger` (vermelho cancelar), `--info` (azul agendada recém-adicionada). Padrão se mantém entre popover, dialog e calendário. Manter

## 3Dívidas técnicas / code smells

### 3.1 Monólitos Vue (≥2.500 LOC)

`AgendaEventDialog.vue` com 6k LOC concentra: cadastro de evento, recorrência, billing config, multas, pacote saldo/upfront, lock de cobrança emitida, preview da série, edição de ocorrência única vs todos. Sete responsabilidades distintas em um único componente — viola SRP.

Sintomas operacionais já observados: edição perigosa (qualquer alteração precisa varrer todo o arquivo), regressões frequentes em flows não-relacionados, teste unitário não consegue isolar um caso sem mockar 80% do contexto.

Quebra natural:

- `AgendaEventDialogShell` — orquestrador, gerencia steps e props
- `AgendaEventDialogStepBasic` — paciente, data, hora, modalidade
- `AgendaEventDialogStepRecurrence` — recorrência + preview de série
- `AgendaEventDialogStepBilling` — service/insurance/billing type/lock
- `AgendaEventDialogStepConfirm` — preview final + diff antes/depois

### 3.2 Triplicação de lógica (Melissa / Rail / Clínica)

Maior risco arquitetural identificado. Os bugs corrigidos hoje em `useMelissaAgenda._applyStatusDecisions` (cobrança dupla, `updated_at` gotcha, link universal) não foram propagados para `AgendaTerapeutaPage` nem `AgendaClinicaPage`.

O caminho que vai dar errado: replicar manualmente os fixes para cada um dos 3 contextos. Multiplica esforço por 3 e introduz drift.

Caminho correto: extrair em `useAgendaStatusChangeOrchestrator.js` consumido pelos três (Melissa via composable, Rail/Clínica via importação direta). Tornar `_applyStatusDecisions`, `_loadStatusChangeContext`, `_needsConfirmDialog` e `_applyStatusUpdateOnly` em funções puras com deps por injeção.

### 3.3 `Promise.allSettled` escondendo falhas

Padrão recorrente: `tasks.push(...); await Promise.allSettled(tasks)`. Quando uma das tasks falha, vira `{status:'rejected'}` e o toast warn genérico aparece — fácil de ignorar.

Caso real (corrigido hoje): UPDATE em `billing_contracts` com campo `updated_at` inexistente. Falha silenciosa por semanas. Foi descoberto manualmente quando o saldo não incrementava em teste E2E manual.

Política sugerida:

- Tasks com efeitos colaterais financeiros usam `await` sequencial + `try/catch` + toast/console específico por falha
- `Promise.allSettled` apenas para operações realmente independentes (ex: pré-buscas de UI auxiliar)
- Logger estruturado com tags por domínio (`[agenda/status]`, `[agenda/billing]`) ao invés de `console.warn` espalhado

### 3.4 `normalizeForMelissa` whitelist silenciosa

A função `normalizeForMelissa(r)` retorna um objeto com campos explícitos. Campos no `r` original que não estão no return somem silenciosamente. Bug do dia: `owner_id` não estava no return → após o watch sync introduzido, virava `null` → INSERT em `financial_records` violava NOT NULL.

Memória existente (`project_pickdbfields_whitelist`) documenta bug análogo no `pickDbFields`. Padrão repetido.

Mitigação imediata: documentar e auditar manualmente todos os campos necessários. Mitigação estrutural: TypeScript ou JSDoc tipado no return — o linter sinalizaria campos faltantes antes do runtime.

### 3.5 Snapshot stale em refs reativas

`eventoSelecionado.value = ev` copia a referência. Quando o `eventos` computed recalcula com novo objeto, a ref guardada não acompanha. Padrão repetido em outros lugares do Melissa (já há memória documentando).

Mitigação aplicada hoje: watch em `M.eventos` com lookup por id + recurrence_id/date (cobre transição virtual→materializada).

Mitigação estrutural: trocar o pattern por `const selectedId = ref(null)` + `const selectedEvent = computed(() => eventos.value.find(e => e.id === selectedId.value))`. Reatividade nativa, zero watches, zero race conditions.

## 3 · Dívidas técnicas (continuação)

### 3.6 Race conditions em `refetch()`

`_reloadRange` dispara queries paralelas (eventos, virtuais, bloqueios, payment state, propagação cross-week). Operações UI sequenciais rápidas (antecipar→revogar→antecipar) podem ler ctx antes do refetch anterior propagar, levando a decrements/increments com base em valores stale.

Mitigação aplicada hoje no bloco `reverseRestoreSaldo`: refetch explícito do `billing_contracts.sessions_used` direto do DB imediatamente antes do UPDATE. Padrão "lê fresh, escreve consciente".

Mitigação estrutural: mover lógica de increment/decrement de saldo para RPC server-side com lock pessimista (`SELECT ... FOR UPDATE`). Elimina race condition no nível de banco.

### 3.7 RPCs sem idempotência forte

`create_financial_record_for_session` foi corrigido para ignorar `cancelled` em idempotência (commit `c23d0a5`, já documentado em memória). Mas o handler do antecipar fazia UPDATE manual em record cancelled após chamar a RPC, reativando-o como paid. Audit trail polluído.

Princípio: RPCs financeiras nunca devem reusar records cancelados. Toda operação que cria cobrança gera record novo. Cancellation é terminal.

### 3.8 Pattern `updated_at` inconsistente entre tabelas

Algumas tabelas têm trigger `set_updated_at` automático (ex: `financial_exceptions`), outras não (`billing_contracts`). Código cliente assume incorretamente que todas têm. Foi origem direta do bug do `updated_at`.

Tabela atual:

Tabela | tem updated_at? | tem trigger?
--- | --- | ---
`financial_records` | ✅ | presume-se sim
`financial_exceptions` | ✅ | ✅ `trg_financial_exceptions_updated_at`
`agenda_eventos` | ✅ | presume-se sim
`recurrence_rules` | ✅ | presume-se sim
`billing_contracts` | ❌ | ❌

Recomendação: adicionar `updated_at` + trigger em `billing_contracts` via migration. Tornar consistente em todas as tabelas de domínio operacional.

### 3.9 RLS pode mascarar erros

UPDATEs em RLS-protected tables retornam silenciosamente 0 rows quando a policy filtra. Sem `RETURNING` explícito o cliente não detecta. No fluxo de status change atual, vários UPDATEs assumem que rodaram.

Mitigação: adicionar `.select()` ou `.select('id').single()` em UPDATEs sensíveis; falhar explicitamente se 0 rows afetadas.

### 3.10 Estado distribuído sem fonte da verdade

Mesma informação (e.g. `sessions_used`) lida de múltiplas fontes: `ctx.billingContract.sessions_used` (snapshot do load), `ev.contract.sessionsUsed` (via ruleContractMap), DB fresh, cached em variáveis locais. Pontos de divergência são pontos de bug.

Princípio: single source of truth por operação. Antes de UPDATE crítico, fresh-fetch + uso somente da leitura recente. Em UIs reativas, derivar de um único store/computed.

## 4Issues de UX

### 4.1 Sobreposição semântica: Usar / Antecipar / Realizada

Em pacote saldo, três botões diferentes podem operar sobre a mesma sessão:

- Usar (popover saldo card) → materializa + realizada + cria record + decrementa
- Antecipar pagamento (popover financeiro) → materializa + cria record paid SEM decrementar
- Realizada (popover status group) → dialog forward com checkbox "Gerar cobrança"

Cada um cobre um caso de uso ligeiramente diferente, mas o overlap confunde. Durante o teste do dia, usuário ficou perdido entre os três caminhos.

Proposta: unified action menu. Um único botão "Marcar sessão" abre menu contextual:

    ┌─ Marcar sessão como… ─────────────┐
    │ ✓ Realizada                       │
    │   ├─ Já recebi (PIX/dinheiro/...) │
    │   └─ Cobrar depois (saldo/avulsa) │
    │ ⚠ Faltou (+ multa/saldo)          │
    │ ✕ Cancelar (+ regra)              │
    ├───────────────────────────────────┤
    │ 💰 Antecipar pagamento (futura)   │
    └───────────────────────────────────┘

### 4.2 Recovery único via "Agendada" — pouco descobrível

Sessão em `cancelado` tem 4 botões disabled (Realizada/Falta/ Reagendar/Cancelar) com tooltip "use Agendada para reativar". O aprendizado depende do hover no tooltip — primeiro encontro pode ser frustrante.

Proposta: badge inline ao lado dos botões disabled, ou banner amarelo no topo do popover: "Sessão encerrada. Clique Agendada para reabrir e permitir novas ações."

### 4.3 Múltiplos records cancelled poluindo audit

Ciclos antecipar/revogar/realizar geram records cancelled empilhados. Em `/financeiro` o filtro padrão `.neq('status','cancelled')` esconde do user normal, mas em queries de debug ou em telas administrativas a poluição aparece.

Propostas:

- UI de "histórico" da sessão (drawer lateral) listando todos os records, agrupando cancelled como timeline colapsada
- Soft-archive: após N dias com sessão em terminal state, mover records cancelled para tabela de audit (`financial_records_archive`)

### 4.4 Dialog "Como cobrar?" / "Já recebi?" — refatorado, mas inspecionar mais flows

Hoje foi refatorado o bloco "Cobrança no pacote saldo" para ter sub-question explícita "A sessão já foi paga?". O mesmo padrão deve ser auditado em outros lugares onde aparece "método de pagamento" com opções mistas:

- Dialog Antecipar pagamento — ainda usa `paymentMethodOptionsCobranca` com "Já recebi — PIX". Padrão novo deveria propagar para consistência.
- Dialog Bloqueio, Dialog Service Quick — mesma checagem.

### 4.5 "A cobrar R$ X" enganoso para pacote saldo

Fix aplicado hoje: label muda para "Aguardando uso do pacote" (saldo) ou "Coberta pelo pacote (upfront)". Verificar consistência em:

- Card no FullCalendar — badge $ suprimido em pacote-tied state=none ✓
- Dialog AgendaEventoFinanceiroPanel — verificar se exibe label correta
- /financeiro lista — pacote saldo session sem record não deve aparecer como "a cobrar"

### 4.6 Feedback de erro fraco em handlers async

Toasts genéricos "Falha ao processar mudança de status" não ajudam usuário. Quando RLS bloqueia ou constraint quebra, mensagem deveria ser específica:

- "Cobrança paga não pode ser editada — use estorno em /financeiro"
- "Saldo do pacote insuficiente — disponíveis: 0 de 4"
- "Sessão tem cobrança vinculada — revogue antes de cancelar"

### 4.7 Botão "Antecipar pagamento" → "Revogar pagamento" alternando

Padrão implementado hoje (alterna baseado em `isAntecipacaoAtiva`). Pode ser confuso para usuário que esquece do que clicou — botão "muda sozinho". Considerar:

- Manter "Antecipar pagamento" sempre visível; quando há pagamento ativo, exibir banner inline acima: "Pagamento antecipado: R$ X via PIX em 20/05.
  "
- Mais info, menos ambiguidade — mostra estado e ação no mesmo bloco.

### 4.8 Reverse dialog: "manter cobrança ativa" potencialmente perigoso

No dialog reverse, radio "Manter cobrança ativa" deixa user reativar sessão com record pending órfão. É uma escolha legítima mas raramente intencional. Considerar adicionar warning quando seleciona "manter".

## 5Riscos arquiteturais

### 5.1 Replicação Rail/Clínica sem refator = duplicação de dívida

Replicar os fixes do dia em `AgendaTerapeutaPage` e `AgendaClinicaPage` mantém as 3 implementações divergentes. Primeira sprint após replicação vai gastar tempo descobrindo "por que funciona em Melissa mas não em Clínica" (ou vice-versa).

Sequência recomendada antes da replicação:

1. Extrair status change orchestrator para composable shared
2. Migrar Melissa para usar o composable shared
3. Testar (a bateria já existe — 13 cenários)
4. Migrar Rail/Clínica para o composable shared (1 commit por persona)

### 5.2 Estado distribuído sem migration testada

Várias decisões de domínio são implícitas:

- Sessão paid + status=cancelado → estado válido ou inválido?
- Sessão sem billing_contract_id mas com record paid em pacote saldo → órfã ou intencional?
- Contract com status='completed' mas sessions_used < total — possível após sessions_used decrementar via reverse

Não há documentação formal das invariantes do domínio. Consequência: cada bug exige reconstrução mental do que deveria ser válido.

Recomendação: documento de invariantes do domínio em `Obsidian/Brain/wiki/agenda-invariantes.md` + (futuramente) constraints SQL ou triggers checando os mais críticos no banco.

### 5.3 Multi-tenant: `tenant_id` manual em todo INSERT

Cada INSERT em `financial_records`, `agenda_eventos`, `billing_contracts` requer `tenant_id` explícito. Esquecer = constraint violation, ou pior, escrever no tenant errado.

Mitigação parcial: RLS bloqueia leitura cross-tenant. Mas writes ainda dependem do código cliente. Pattern de "service layer" com tenant injetado centralmente reduziria a área de risco.

### 5.4 Edge functions desacopladas da migration story

Edge functions (asaas-webhook, evolution-whatsapp, etc) operam direto no DB e podem deixar state inconsistente com o que o frontend esperar. Não foi escopo deste relatório, mas merece auditoria separada.

### 5.5 Ausência de feature flags

Cada fix do dia foi merged direto pra main e deploy implícito. Não há feature flag para experimentar "novo dialog reverse" em produção sem expor para todos. Em sistema com clientes pagantes, é fragil.

### 5.6 Backfills manuais via psql não rastreados

Durante o teste, vários `UPDATE billing_contracts SET sessions_used=X` foram aplicados manualmente para limpar estado. Em produção isso é impraticável e arriscado. Falta:

- Migration ferramenta para "operações de correção de dados" com versionamento e rollback
- Audit log de operações administrativas sobre dados

### 5.7 Indicador de saldo derivado vs persistido

`billing_contracts.sessions_used` é INTEIRO incrementado/ decrementado manualmente. Verdade "real" é o COUNT de records associados. Risco: drift entre os dois ao longo do tempo.

Alternativa: tornar `sessions_used` uma VIEW computed via `COUNT(*) FROM financial_records WHERE billing_contract_id=X AND status IN ('paid','pending')`. Performance pode requerer materialização ou índice; mas elimina classe inteira de bugs de sync.

## 6Recomendações priorizadas

Priorização: P0 = antes da próxima replicação (Rail/Clínica). P1 = sprint atual ou próximo. P2 = quando couber, valor compounding. P3 = ideal mas não bloqueante.

# | Recomendação | Prio | Esforço | Impacto
--- | --- | --- | --- | ---
R1 | Extrair status change orchestrator para composable shared `useAgendaStatusChangeOrchestrator.js`. Funções puras (loadContext, needsDialog, applyDecisions, applyStatusUpdate). Melissa migra primeiro, depois Rail/Clínica importam. | P0 | 4–6h | Alto · elimina trio
R2 | Migration: adicionar `updated_at` + trigger em `billing_contracts`. Elimina o gotcha que custou horas hoje. Padroniza com outras tabelas. | P0 | 30min | Alto · elimina classe
R3 | Auditar Promise.allSettled em fluxos financeiros. Trocar por awaits sequenciais com try/catch dedicado. Adicionar logger estruturado (`[agenda/billing/saldo]`). | P0 | 2–3h | Alto · visibilidade
R4 | Documentar invariantes do domínio em `Obsidian/Brain/wiki/agenda-invariantes.md`. Estado válido de status × records × contract. Referência consultável. | P0 | 1–2h | Médio-alto · onboarding
R5 | Refatorar `AgendaEventDialog.vue` em 5 sub-componentes (Shell, StepBasic, StepRecurrence, StepBilling, StepConfirm). Manter API atual; migration interna. | P1 | 2–3 dias | Alto · manutenção
R6 | Trocar `eventoSelecionado` snapshot por `selectedId` + computed. Elimina snapshot stale. Replicar em outros refs do Melissa. | P1 | 1–2h | Médio · estabilidade
R7 | Auditoria visual da sessão — drawer com timeline de records (pending/paid/cancelled), com cancelled colapsados por default. Reduz poluição em `/financeiro`. | P1 | 1 dia | Médio · UX
R8 | RPCs server-side para increment/decrement de saldo com `SELECT FOR UPDATE`. Elimina race condition em fluxos rápidos. | P1 | 3–4h | Médio-alto · correctness
R9 | Mensagens de erro específicas em handlers de status/billing. Catalogar erros típicos (RLS, constraint, state inválido). | P1 | 2h | Médio · UX
R10 | TypeScript ou JSDoc tipado em `normalizeForMelissa`, `pickDbFields` e funções de transformação de dados. Lint flagga campos faltantes. | P2 | 2–4h por área | Alto longo prazo
R11 | Testes E2E (Playwright) dos 13 cenários. Substitui bateria manual. CI roda em cada PR. | P2 | 1 semana | Alto · confiança
R12 | Unified action menu "Marcar sessão" substituindo os 3 botões sobrepostos (Usar/Antecipar/Realizada). | P2 | 1 dia | Médio · clarity
R13 | Feature flags via `tenant_settings` ou biblioteca específica. Permite rollout gradual de mudanças no dialog. | P3 | 1 dia | Baixo curto, alto longo
R14 | `sessions_used` como VIEW computed em vez de coluna incrementada. Elimina drift de sincronia. Avaliar performance. | P3 | 1–2 dias | Alto correctness, médio risco
R15 | Ferramenta de "data fix" versionada para correções administrativas em produção. Audit log obrigatório. | P3 | 2–3 dias | Alto operacional

Sequência sugerida: R2 (migration updated_at) → R1 (extrair orchestrator) → R4 (doc invariantes) → R3 (audit Promise.allSettled) → replicar Rail/Clínica. Tudo P0 entrega antes da próxima replicação reduz drasticamente o risco de regressão.

## 7Roadmap proposto

### Sprint imediato (1–2 semanas) — Consolidação P0

1. Dia 1 · R2 (migration `updated_at` + trigger billing_contracts) · 30 min
2. Dia 1–2 · R3 (audit Promise.allSettled) · 2–3h
   - Grep `Promise.allSettled` em todo src/
   - Reescrever blocos financeiros para awaits sequenciais
   - Logger por domínio
3. Dia 2–3 · R4 (doc invariantes) · 1–2h
4. Dia 3–5 · R1 (extrair orchestrator) · 4–6h
5. Dia 5–6 · Replicar fixes pra Rail (AgendaTerapeutaPage) via composable shared · 4–6h
6. Dia 6–7 · Replicar fixes pra Clínica (AgendaClinicaPage) · 4–6h
7. Dia 8 · Bateria de teste manual nos 3 personas · 1 dia

### Sprint seguinte (2–3 semanas) — Refator P1

1. R5 — Refator `AgendaEventDialog` em 5 sub-componentes (2–3 dias)
2. R6 — Snapshot stale fix global (2h)
3. R7 — Drawer de audit history (1 dia)
4. R8 — RPCs increment/decrement com lock (3–4h)
5. R9 — Mensagens de erro catalogadas (2h)
6. Iteração de UX C12 (consolidar Usar/Antecipar/Realizada) · 1 dia

### Próximo trimestre — Maturação P2

1. R10 — JSDoc tipado em normalizers (incremental por arquivo)
2. R11 — Testes E2E Playwright dos 13 cenários (1 semana)
3. R12 — Unified action menu (refator UX)
4. Auditoria de Edge Functions vs frontend (sprint dedicado)

### Backlog longo prazo P3

- R13 — Feature flags infrastructure
- R14 — `sessions_used` como VIEW
- R15 — Data-fix tooling
- Migração TypeScript completa (decisão de produto)

## Considerações finais

O módulo de agenda é o coração operacional do AgênciaPSI — é por onde o terapeuta vive o dia. Investimentos em estabilidade aqui têm retorno desproporcional: cada bug evitado é uma sessão real do paciente que não vira incidente.

A bateria de testes do dia provou que a metodologia funciona: 13 cenários documentados + bateria manual + iteração rápida descobriu mais bugs em 1 dia do que toda a auditoria anterior. Manter esse loop e automatizá-lo (R11) é o investimento de maior alavancagem.

O risco maior não é o que já encontramos — esses estão consertados. É o que ainda não foi exercitado em Rail e Clínica. A janela para consolidar antes de replicar é agora.

A boa notícia: a base é sólida. Os bugs encontrados foram de execução, não de design. As decisões arquiteturais maiores (SimplePractice-style invariantes, status change centralizado, audit trail em notes, separação avulsa/pacote) provaram-se corretas em uso real. O trabalho é polir, não reescrever.

Relatório gerado em 2026-05-20 · Análise pós-bateria C1–C13 · Sistema AgênciaPSI v5.0 ·
Baseado em: 15 commits do dia, 14 bugs corrigidos, 9 memórias de sessão, ~22k LOC inspecionadas nos hotspots.

## 8Apêndices

### A · Bugs corrigidos em 2026-05-20

# | Bug | Severidade | Commit
--- | --- | --- | ---
1 | Cobrança dupla em multa: `_applyStatusDecisions` inseria multa mas deixava original pending | Crítico | `d6423da`
2 | `'fixed'` vs `'fixed_fee'` off-by-key em `calcChargeAmount` | Médio (dormente) | `d6423da`
3 | Label "Como cobrar?" com options "Já recebi" misturadas — usuário marcou paid achando que era cobrar | Alto UX | `079509e`
4 | `billing_contracts.updated_at` inexistente causando UPDATE silently falhar em `Promise.allSettled` | Crítico | `16dfa02`
5 | Reverse transitions deixavam multa órfã (faltou→agendado) | Alto | `5684297`
6 | `consumeSaldo` não amarrava `billing_contract_id` ao evento | Alto | `3f3f2ac`
7 | Badge $ amber em sessão cancelada | Médio | `753182c`
8 | `paymentLabel` usava `ev.price` em vez de `paymentAmount` para pending | Médio | `753182c`
9 | "Gerar fatura" disponível em sessão encerrada | Alto | `753182c`
10 | `_reloadRange` não destruturado em `_buildHandlers(deps)` | Médio | `753182c`
11 | Faltou+multa-sem-consume não amarrava `billing_contract_id` | Médio | `5965b05`
12 | Re-antecipar reutilizava record cancelled (audit trail confuso) | Alto | `b5e00a7`
13 | Popover snapshot stale após materialização virtual→real | Alto | `b5e00a7` + `f83315b`
14 | `normalizeForMelissa` não expunha `owner_id` → INSERT null violation | Crítico | `7d2a405`

### B · Pendências mapeadas em memória (pós-C13)

- `project_c12_antecipar_iterar` — iterar UX da antecipação (deferido pelo user)
- `project_melissa_popover_snapshot` — refator pra computed (mitigado mas não estrutural)
- `project_billing_contracts_no_updated_at` — gotcha documentado, fix em R2
- `project_pendencia_doc_ajuda` — doc completa para área de ajuda pós Fase 9
- Cleanup: sessão Otto Rank 5364f631 com record pending leftover

### C · Commits do dia (20/05)

    4da0bc2 HANDOFF + log: C12 deferred · testando C13
    f83315b agenda: popover watch acompanha transicao virtual->materializada
    7d2a405 agenda: normalizeForMelissa expoe owner_id/tenant_id/contract_id
    b5e00a7 agenda: popover sincroniza com eventos + antecipar nao reusa cancelled
    272c804 agenda: revogar antecipacao de pagamento
    00c4168 agenda: C12 prep — detectar paid pre-existente em pacote saldo realizada
    9ead3fd HANDOFF + log: C11 fechado · 4/4 sub-testes OK · proximo C12
    5965b05 agenda: link universal pacote + refresh saldo no reverse
    45984e8 agenda: label + badge cientes de pacote em sessoes state=none
    3f3f2ac agenda: consumeSaldo agora amarra billing_contract_id no evento
    5684297 agenda: reverse transition trava (Agendada apos artefatos)
    16dfa02 agenda pacote saldo: fix root cause + sequential awaits
    079509e agenda: dialog pacote saldo realizada — 2 sub-questions claras
    7dc7dce wiki: log session C10 fechado completo
    1e74a11 HANDOFF: C10 fechado · 5/5 sub-testes OK · proximo C11
    753182c agenda: C10 pos-test fixes + lock sessao encerrada + addendum doc
    3caf579 agenda popover: botao Agendada + fixes pos-C10/B
    d6423da agenda: pre-C10 fix _applyStatusDecisions cancela pendingRecord

### D · Status dos cenários documentados

# | Cenário | Persona testada | Status
--- | --- | --- | ---
C1–C9 | Bloqueio, avulsas, recorrência, pacote, per_session | Melissa | OK (anteriormente)
C10 | Status change avulsa (5 sub-testes) | Melissa | OK
C11 | Status change pacote saldo (4 sub-testes) | Melissa | OK
C12 | Antecipar pagamento | Melissa | DB OK · UX a iterar
C13 | Edit cobrada (imutabilidade) | Melissa | Em teste agora
— | Rail (AgendaTerapeutaPage) | Rail | Pendente — após refator
— | Clínica (AgendaClinicaPage) | Clínica | Pendente — após refator

### E · Referências internas

- `src/docs/agenda-compromisso-financeiro-cenarios.html` — doc vivo dos 13 cenários (com addendum C10)
- `HANDOFF.md` — estado de continuidade da sessão
- `Obsidian/Brain/log.md` — log cronológico
- `Obsidian/Brain/wiki/agenda-compromisso-fluxo.md` — fluxo conceitual
- `~/.claude/projects/.../memory/MEMORY.md` — índice de memórias persistentes

Fim do relatório · 10 páginas · Geração local sem dependências externas · Para PDF, abrir no browser e Ctrl+P → Salvar como PDF.