✦ addendum Implementado em 20/05 (C10) — divergências e melhorias vs mockup
O mockup original deste doc foi escrito antes da implementação real. Durante a bateria de testes C10 (status change avulsa), surgiram bugs, melhorias UX e travas que foram implementadas mas não estão refletidas nas seções abaixo. Este addendum captura essas mudanças. Cenários C1-C9 continuam fiéis ao mockup; C10 deve ser lido com este addendum em mente.
O que ficou diferente / melhor que o mockup original
_applyStatusDecisions INSERIA o record da multa MAS deixava o original pending → cobrança dupla (R$ 200 + R$ 30 = R$ 230). Fix em useMelissaAgenda.js:1450-1505: aplicar multa agora cancela o ctx.pendingRecord com nota de auditoria em notes ("[YYYY-MM-DD] Cancelada — substituída por multa de no-show"). Description do novo record carrega data da sessão pra paciente identificar na fatura: "Multa por falta · sessão dd/mm/aa". ✅ Match com o mock C10/b.
min_hours_notice novoAgendaStatusChangeConfirmDialog, embaixo do checkbox aparece texto explicando por que veio (des)marcado por padrão:
- > janela: "Cancelou 18.5h antes da sessão. Regra: multa apenas quando cancelamento ocorre com menos de 2h de antecedência → sem multa por padrão."
- < janela: "Cancelou 45min antes da sessão (menos que os 2h da regra) → multa aplicada por padrão."
- Após início: "Cancelou 0.5h após o início da sessão (menos que os 2h da regra) → multa aplicada por padrão."
--info cyan) | Realizada | Falta | Reagendar | Cancelar. Permite reset de status (realizado/faltou/cancelado → agendado) direto do popover sem precisar abrir o AgendaEventDialog completo. Único caminho de saída do estado encerrado (ver item 5).
status='cancelado'+ sem record ativo → "Sessão cancelada · sem cobrança ativa"status='faltou'+ sem record ativo → "Sessão não realizada · sem cobrança ativa"- Multa pending continua mostrando "A receber R$ X (pendente)" normalmente
paymentLabel agora usa paymentAmount também pra 'pending' (antes só pra 'paid'; multa de R$ 30 mostrava R$ 150 do ev.price original).
cancelado ou faltou bloqueia ações que abrem porta pra dados inconsistentes:
- Botão "Editar sessão" some do popover
- Botão "Gerar cobrança" some do
AgendaEventoFinanceiroPanel(dentro do AgendaEventDialog) — antes dava pra emitir fatura nova mesmo em sessão cancelada - Botões Realizada / Falta / Reagendar / Cancelar ficam
disabledcom tooltip "Sessão encerrada — use Agendada pra reativar antes" - SÓ Agendada continua funcional (caminho explícito de recuperação caso tenha sido marcado por engano)
- Badge $ amber some do card no FullCalendar (sessão encerrada + record cancelled → no badge)
_reloadRange() após status change fixonUpdateSeriesEvent não chamava _reloadRange() após _applyStatusDecisions — badge $ e label "A receber" ficavam stale até trocar de view ou F5. Fix: reload no fim do flow. Bug paralelo: _reloadRange não estava destruturado em _buildHandlers(deps) (era passado em deps mas não desempacotado) → toast "ReferenceError: _reloadRange is not defined" ao tentar reload. Ambos corrigidos.
useAgendaFinanceiro.js fixcalcChargeAmount comparava charge_mode === 'fixed', mas o schema usa 'fixed_fee'. Off-by-key silencioso que caía no fallback. Path não exercitado na Melissa (que usa _applyStatusDecisions, não handleStatusChange), mas iria quebrar se algum dia fosse. Fix: 'fixed_fee'.
Pendências mapeadas durante C10 — implementar pós-C13
/financeiro. Solução planejada: confirm dialog ao reverter de cancelado/faltou pra agendado com record/multa pending → oferecer auto-cancelar a multa também (radio sim/não). Memória salva em project_agenda_reverse_transitions.md.
eventoSelecionado.value é setado uma vez em abrirEvento(ev) — quando _paymentStateMap updata depois (ex: bulk-load assíncrono pós F5 leva 1-3s), o popover NÃO re-renderiza com state novo. Caso típico: F5 + clique rapidíssimo no card → popover mostra "A cobrar R$ 150" (state='none' default) porque snapshot pegou map vazio. Fix planejado: guardar ev.id em vez de ev, popover deriva via computed eventos.value.find(...). Memória em project_melissa_popover_snapshot.md.
financial_records.status='pending' em vez de 'paid'. A investigar pós-C13 — pode ser que o reset/realizada de novo tenha sobrescrito, ou o markPaid não tenha entrado no caminho de UPDATE.
Indicadores visuais de pagamento
Como o sistema mostra "essa sessão ainda não foi paga" sem que você precise abrir o financeiro pra checar.
Na agenda — 3 estados
- Pendente: sessão com paciente, cobrança gerada mas não paga (ou nenhuma cobrança ainda).
- Pago: record marcado como
paidem/financeiro. Pra ver de relance quais sessões já estão quitadas. - Neutro: sem cobrança (compromisso pessoal, bloqueio) ou ocorrência virtual de pacote upfront/saldo — controlados pelo contrato.
- Os 3 canais não competem: esquerda = pago, direita = pendente, nada = neutro.
No popover da sessão — linha "A receber"
- Linha em destaque amber logo abaixo do horário, com ícone $.
- Texto adapta-se ao caso:
- "A cobrar R$ X" — quando ainda não há cobrança gerada (sessão "Não cobrar").
- "A receber R$ X (cobrança pendente)" — quando o record existe mas não foi pago.
- "Cobrança ainda não gerada" — quando a sessão não tem valor definido.
- Some quando a sessão fica paga.
1 Bloqueio · sem paciente, sem cobrança
Almoço, feriado, férias ou compromisso pessoal. Aparece cinza no calendário e não gera lançamento no financeiro.
- Abra
BloqueioDialogpelo menu da agenda, escolha modo Período e crie um bloqueio de 15/05 a 17/05 com motivo "Férias curtas". - Confira que o intervalo aparece em cinza nas 3 agendas (Melissa / Rail / Clínica) e que nada foi gerado em
/financeiro. - Clique num horário dentro do bloqueio (ex: 16/05 às 10h). O
AgendaEventDialogabre normalmente, com um aviso amarelo no topo avisando que aquele slot está bloqueado. Você pode prosseguir e criar o compromisso assim mesmo — o aviso é só pra evitar acidente.
BloqueioDialog · "Período"
Financeiro · Lançamentos
AgendaEventDialog · escolha do tipo
O que acontece
Você decide. Diferente do agendador público (que veta agendamento em bloqueio), aqui o sistema só te avisa. É comum querer encaixar uma sessão sobre um almoço, ou agendar uma reunião em pleno feriado.
- Se prosseguir, o compromisso é criado normalmente e aparece por cima da faixa cinza do bloqueio.
- Se quiser cancelar, basta fechar o dialog — nenhum dado é gravado.
- O aviso usa o título do bloqueio (ex: "Férias curtas") pra você lembrar o motivo do bloqueio antes de decidir.
2 Avulsa · sem cobrança chargeMode = none
Sessão única, terapeuta decide depois se cobra (modo manual / fechamento mensal).
/financeiro.
/financeiro.
AgendaEventDialog · Ana Souza Ferreira
Você pode gerar a cobrança depois pelo /financeiro ou pelo painel da sessão.
Financeiro · Ana
3 Avulsa · cobrar ao salvar particular
Gera lançamento pendente no ato. Default da agenda em modo manual.
/financeiro com status pendente, payment_method = pix e paid_at = null.
/financeiro.
AgendaEventDialog · Henrique
Só aparece quando a forma de pagamento não é o link Asaas (que é sempre liquidado pelo gateway).
Financeiro · Henrique
4 Avulsa · "já recebi" no momento da criação
Quando o paciente paga PIX/dinheiro na hora — record já nasce pago.
/financeiro com status pago, payment_method = pix e paid_at = hoje.
Joyce · Sessão R$ 180
Quando escolhe "Já recebi", o record nasce com paid_at = agora + status = paid.
Financeiro · Joyce
5 Avulsa · convênio
Valor pago pelo plano de saúde, não pelo paciente. Lançamento vinculado ao plano.
InsurancePlanQuickCreateDialog se não existir), valor R$ 95. Salvar. Verificar record com insurance_plan_id preenchido e pill "convênio" visível.
Sándor · sessão por convênio
Cobrança vai pra fechamento mensal do convênio — não cobra o paciente.
Financeiro · Sándor
6 Recorrente · sem pacote chargeMode = none
Série semanal, mas cobrança decidida sessão a sessão (status driven ou manual depois).
agenda_eventos + 1 row em recurrence_rules; outras 3 ocorrências expandidas em runtime. Zero em /financeiro.
Anna Freud · semanal · 4 sessões
Modelo "1 real + N-1 virtual"
Só a 1ª ocorrência vira row em agenda_eventos. As demais são rec::ruleId::date expandidas em runtime.
Financeiro · Anna Freud
7 Recorrente · pacote único UPFRONT
1 cobrança do valor total no salvar. Cada sessão posterior é "consumo" do já pago.
/financeiro + 1 billing_contract com charging_style=upfront, sessions_used=0.
Donald Winnicott · 4× R$ 200 = R$ 800
Financeiro · Donald
8 Recorrente · pacote único SALDO
Sem record financeiro inicial. Cria billing_contract com sessions_total. Records nascem 1 a 1 quando sessão é realizada.
/financeiro + 1 billing_contract com charging_style=saldo, sessions_total=4, sessions_used=0.
Carl Jung · 4× R$ 40 = R$ 160
Cobra 1 record por sessão realizada. Pacote dá direito ao preço promocional + controle de saldo.
Financeiro · Carl Jung
9 Recorrente · 1 cobrança por sessão sem pacote
Cobra individualmente cada ocorrência. Diferença pro Cenário 6: aqui já materializa records pendentes pra cada ocorrência futura.
/financeiro (1 por ocorrência) com agenda_evento_id apontando pra cada uma.
Michael Balint · semanal
Financeiro · Michael
10 Status change · avulsa pendente
Sessão criada com chargeMode=session, agora chegou a hora. Dialog confirm aparece em realizado/faltou/cancelado.
Maria Magali · 15/05 16:00 · pendente
Cada um abre o AgendaStatusChangeConfirmDialog com variante diferente.
Financeiro · Maria
11 Status change · ocorrência de pacote saldo
Carl Jung tem pacote 4/4. Marca cada sessão como realizada → cria record + decrementa saldo. Faltou → vai pra config default_consume_on_miss.
Carl Jung · pacote saldo
rec::...::2026-05-15O que acontece no banco?
- Materializa a ocorrência virtual: INSERT em
agenda_eventoscomrecurrence_id + recurrence_date - UPDATE
billing_contracts.sessions_used += 1 - INSERT em
financial_recordsR$ 40 status=pending (ou paid se já houver record do antecipar)
Financeiro · Carl Jung
12 Antecipar pagamento · pacote saldo novo · 14/05
Paciente pediu pra pagar a próxima sessão antes dela acontecer. Cria record pago, MAS não decrementa saldo (só quando marcar realizada depois).
/financeiro, MAS saldo do contrato continua intacto. (b) Depois marcar a mesma sessão como Realizada: dialog NÃO deve perguntar "gerar cobrança" (detecta record paid), só atualiza status + decrementa saldo.
Popover da ocorrência virtual
Diferença pra Realizada: não decrementa sessions_used. Quando user marcar Realizada depois, handler detecta record já paid → só atualiza status.
Financeiro · Carl Jung
13 Edit de sessão já cobrada imutável
Invariante "cobrança emitida é imutável pelo dialog da agenda" (Decisão #5, padrão SimplePractice).
AgendaEventDialog. Verificar: (a) aviso amarelo no topo "esta sessão tem cobrança paga"; (b) campos de valor / modalidade / tipo travados (disabled); (c) apenas horário e observações editáveis; (d) tentar mudar valor não persiste.
AgendaEventDialog · Sessão paga
Apenas horário / observações podem ser editados.