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.
AgendaEventDialog.vue (6.070), MelissaLayout.vue (4.331),
AgendaTerapeutaPage.vue (3.567). Refator sustentado é inviável neste estado.
P0
billing_contracts.updated_at —
coluna inexistente. UPDATEs falhavam silenciosamente em
Promise.allSettled. Patch tipo "trigger SQL"
eliminaria a categoria inteira.
P1
eventoSelecionado.value
é cópia, não referência reativa. Padrão repetido em outros componentes do
Melissa. Ergonomia ruim para mutações otimistas.
P1
/financeiro sem filtro. Falta UI dedicada de auditoria.
P2
| 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.
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)
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.
Análise reconhece méritos antes de criticar. O que está bem feito:
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
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
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
@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
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
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
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
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
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 propsAgendaEventDialogStepBasic — paciente, data, hora, modalidadeAgendaEventDialogStepRecurrence — recorrência + preview de sérieAgendaEventDialogStepBilling — service/insurance/billing type/lockAgendaEventDialogStepConfirm — preview final + diff antes/depoisuseMelissaAgenda._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.
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:
await sequencial
+ try/catch + toast/console específico por falhaPromise.allSettled apenas para operações realmente
independentes (ex: pré-buscas de UI auxiliar)[agenda/status], [agenda/billing]) ao invés de
console.warn espalhadonormalizeForMelissa 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.
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.
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.
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.
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.
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.
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.
Em pacote saldo, três botões diferentes podem operar sobre a mesma sessão:
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) │
└───────────────────────────────────┘
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."
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:
financial_records_archive)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:
paymentMethodOptionsCobranca com "Já recebi — PIX". Padrão
novo deveria propagar para consistência.Fix aplicado hoje: label muda para "Aguardando uso do pacote" (saldo) ou "Coberta pelo pacote (upfront)". Verificar consistência em:
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:
Padrão implementado hoje (alterna baseado em isAntecipacaoAtiva).
Pode ser confuso para usuário que esquece do que clicou — botão "muda
sozinho". Considerar:
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".
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:
Várias decisões de domínio são implícitas:
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.
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.
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.
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.
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:
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.
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 |
updated_at +
trigger billing_contracts) · 30 min
Promise.allSettled em todo src/AgendaEventDialog em 5 sub-componentes (2–3 dias)sessions_used como VIEWO 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.
| # | 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 |
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 R2project_pendencia_doc_ajuda — doc completa para área de ajuda pós Fase 94da0bc2 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
| # | 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 |
src/docs/agenda-compromisso-financeiro-cenarios.html — doc vivo dos 13 cenários (com addendum C10)HANDOFF.md — estado de continuidade da sessãoObsidian/Brain/log.md — log cronológicoObsidian/Brain/wiki/agenda-compromisso-fluxo.md — fluxo conceitual~/.claude/projects/.../memory/MEMORY.md — índice de memórias persistentes