Agenda · Compromisso → Financeiro cenários (doc viva)

Esquerda: o que o terapeuta vê na agenda. Direita: o que aparece no financeiro do paciente.

v1 · 2026-05-15 Fase 5 em teste

✦ 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

1. Multa cancela original + cria novo
Antes do fix: _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.
2. Hint contextual explicando regra min_hours_notice novo
No bloco "Aplicar multa?" do AgendaStatusChangeConfirmDialog, 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."
Terapeuta vê a razão da pré-seleção e pode inverter conscientemente.
3. Botão "Agendada" no popover novo
O grupo "Marcar sessão como:" agora tem 5 botões (antes 4): Agendada (pi-calendar, variante --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).
4. Label do popover muda em sessão encerrada UX
Antes mostrava "A cobrar R$ 150" + botão "Gerar fatura" mesmo em sessão cancelada — sugeria que dava pra cobrar uma sessão que não aconteceu. Agora:
  • 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
Bug paralelo fixado: paymentLabel agora usa paymentAmount também pra 'pending' (antes só pra 'paid'; multa de R$ 30 mostrava R$ 150 do ev.price original).
5. Lock em sessão encerrada (cancelado/faltou) trava
Sessão com status 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 disabled com tooltip "Sessão encerrada — use Agendada pra reativar antes"
  • 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)
6. _reloadRange() após status change fix
onUpdateSeriesEvent 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.
7. Bug dormente em useAgendaFinanceiro.js fix
calcChargeAmount 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

⚠ Reverse transitions com multa órfã
Caso: terapeuta marca "Faltou" com multa R$ 30 → percebe que foi engano → clica "Agendada" pra reativar → status volta pra agendado MAS multa R$ 30 fica pending órfã. Hoje precisa cancelar manualmente em /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.
⚠ Popover Melissa = snapshot do clique
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.
⚠ A2 do João Almeida com markPaid não persistiu
Durante teste C10/A2, usuário marcou Realizada + "Sim, registrar pagamento" + Maquininha. Toast verde, card mudou visual, mas DB mostra 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 · badge $ amber (canto direito)
$
Ana Souza (14h-14:50h)
agendado presencial
Pago · barra verde (borda esquerda)
Joyce (15h-15:50h)
agendado presencial
Sem cobrança · neutro
Maria (16h-16:50h)
agendado presencial
  • Pendente: sessão com paciente, cobrança gerada mas não paga (ou nenhuma cobrança ainda).
  • Pago: record marcado como paid em /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"

14:00 – 14:50 · 50min
A receber R$ 200,00 (cobrança pendente)
Presencial
Agendado
  • 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.

Como testar:
  1. Abra BloqueioDialog pelo menu da agenda, escolha modo Período e crie um bloqueio de 15/05 a 17/05 com motivo "Férias curtas".
  2. Confira que o intervalo aparece em cinza nas 3 agendas (Melissa / Rail / Clínica) e que nada foi gerado em /financeiro.
  3. Clique num horário dentro do bloqueio (ex: 16/05 às 10h). O AgendaEventDialog abre 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.
1a · Criar o bloqueio

BloqueioDialog · "Período"

Modo
Período
15/05/2026 → 17/05/2026
Motivo
Férias curtas
Renderizado como faixa cinza nas 3 agendas (Melissa / Rail / Clínica). Não afeta o financeiro.

Financeiro · Lançamentos

Nada gerado. Bloqueio não é cobrável.
1b · Agendar sobre um bloqueio (aviso, não impede)

AgendaEventDialog · escolha do tipo

Este horário está dentro do bloqueio "Férias curtas". O compromisso pode ser criado mesmo assim.
Selecione o tipo de compromisso para começar.
Sessão
Reunião
Outro
O aviso aparece dentro do dialog (não como toast solto) e some sozinho ao reabrir o dialog em outro horário.

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.
No agendador público (link de marcação que o paciente acessa) o comportamento é diferente: se o dia inteiro estiver bloqueado, sessões ficam vetadas e aparece a mensagem "Este dia está bloqueado. Compromissos do tipo Sessão não podem ser agendados." — só você (terapeuta) pode passar por cima.

2 Avulsa · sem cobrança chargeMode = none

Sessão única, terapeuta decide depois se cobra (modo manual / fechamento mensal).

Como testar: Criar sessão pra Ana Souza Ferreira em 15/05 14:00, modalidade presencial, tipo avulsa, deixar "Não cobrar agora" selecionado no card Cobrança ao salvar. Salvar. Verificar evento na agenda (status pendente) e zero record em /financeiro.
Indicadores visuais: Como a sessão ainda não foi cobrada, o evento na agenda mostra um círculo amber com cifrão ($) no canto superior direito. Ao clicar no evento, o popover mostra uma linha amber com o ícone $: "A cobrar R$ X,XX" (ou "Cobrança ainda não gerada" se a sessão não tiver valor). Assim você lembra de gerar a cobrança depois no /financeiro.

AgendaEventDialog · Ana Souza Ferreira

Início
15/05 14:00
Duração
50 min
Modalidade
Presencial
Tipo
Avulsa
Cobrança ao salvar

Você pode gerar a cobrança depois pelo /financeiro ou pelo painel da sessão.

Financeiro · Ana

Nenhum lançamento gerado. Sessão fica como pendente na agenda só.

3 Avulsa · cobrar ao salvar particular

Gera lançamento pendente no ato. Default da agenda em modo manual.

Como testar: Criar sessão pra Henrique, serviço Psicoterapia R$ 200, modalidade online. No card "Cobrança ao salvar", escolher "Gerar cobrança". Forma de pagamento: selecionar PIX. Status do pagamento: deixar em "Cobrança pendente" (default). Salvar. Verificar 1 record em /financeiro com status pendente, payment_method = pix e paid_at = null.
Indicadores visuais: A cobrança foi gerada mas o paciente ainda não pagou. O evento na agenda mostra o círculo amber com $ no canto. No popover, linha amber: "A receber R$ 200,00 (cobrança pendente)". O badge some automaticamente assim que o record for marcado como pago no /financeiro.

AgendaEventDialog · Henrique

Sessão
Psicoterapia · 50 min · R$ 200,00
Modalidade
Online
Cobrança ao salvar
Forma de pagamento
Status do pagamento

Só aparece quando a forma de pagamento não é o link Asaas (que é sempre liquidado pelo gateway).

Financeiro · Henrique

Clique em Salvar sessão

4 Avulsa · "já recebi" no momento da criação

Quando o paciente paga PIX/dinheiro na hora — record já nasce pago.

Como testar: Criar sessão pra Joyce, valor R$ 180. No card "Cobrança ao salvar", escolher "Gerar cobrança". Forma de pagamento: PIX. Status do pagamento: "Já recebi (dar baixa)". Salvar. Verificar 1 record em /financeiro com status pago, payment_method = pix e paid_at = hoje.
Indicadores visuais: O pagamento já foi confirmado, então o evento ganha uma barra verde fina na borda esquerda (substitui a cor padrão do commitment naquele lado). Sem badge $ no canto e o popover não mostra linha de cobrança. É o estado-alvo: dá pra olhar a agenda e identificar de relance quais sessões já foram pagas.

Joyce · Sessão R$ 180

Cobrança ao salvar
Forma de pagamento
Status do pagamento

Quando escolhe "Já recebi", o record nasce com paid_at = agora + status = paid.

Financeiro · Joyce

Clique em Salvar sessão

5 Avulsa · convênio

Valor pago pelo plano de saúde, não pelo paciente. Lançamento vinculado ao plano.

Como testar: Criar sessão pra Sándor com convênio Unimed Nacional (criar via InsurancePlanQuickCreateDialog se não existir), valor R$ 95. Salvar. Verificar record com insurance_plan_id preenchido e pill "convênio" visível.
Indicadores visuais: Como o convênio ainda não fechou o mês, o record fica pendente — o evento na agenda mostra badge $ e o popover mostra "A receber R$ 95,00 (cobrança pendente)". Quando você marcar o record do convênio como pago após o fechamento mensal, os indicadores somem.

Sándor · sessão por convênio

Convênio
Unimed Nacional
Valor (clínica recebe)
R$ 95,00

Cobrança vai pra fechamento mensal do convênio — não cobra o paciente.

Financeiro · Sándor

Sessão 15/05 14:00
via convênio Unimed
convênio
R$ 95,00
Vai pra fatura mensal do convênio (não cobra paciente individualmente).

6 Recorrente · sem pacote chargeMode = none

Série semanal, mas cobrança decidida sessão a sessão (status driven ou manual depois).

Como testar: Criar série recorrente pra Anna Freud, frequência semanal, 4 ocorrências, modo de cobrança "Não cobrar". Salvar. Verificar no banco: 1 row em agenda_eventos + 1 row em recurrence_rules; outras 3 ocorrências expandidas em runtime. Zero em /financeiro.
Indicadores visuais: Apenas a 1ª ocorrência (materializada de verdade) mostra o badge $ e a linha "A cobrar". As outras 3 (virtuais, expandidas em runtime) ficam limpas — vão ganhar o badge automaticamente quando virarem ocorrências reais (ao marcar status ou editar). Isso evita poluir o calendário com $ em séries longas.

Anna Freud · semanal · 4 sessões

Frequência
Semanal · qua 14:00
Ocorrências
4 (1 real + 3 virtuais)
Modo de cobrança
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

Zero lançamentos. Cobra sessão por sessão quando marcar como Realizada.

7 Recorrente · pacote único UPFRONT

1 cobrança do valor total no salvar. Cada sessão posterior é "consumo" do já pago.

Como testar: Criar série pra Donald Winnicott, 4 sessões semanais R$ 200 = R$ 800 total, modo "Pacote único", estilo "Upfront", forma "Enviar link Asaas". Salvar. Verificar 1 record R$ 800 pending em /financeiro + 1 billing_contract com charging_style=upfront, sessions_used=0.
Indicadores visuais: A 1ª ocorrência (que carrega o record de R$ 800 do pacote) mostra o badge $ enquanto o boleto Asaas não for pago — popover diz "A receber R$ 800,00 (cobrança pendente)". As outras 3 ocorrências (virtuais) ficam limpas, mesmo que o paciente não tenha pago ainda — porque o controle é a nível de pacote, não sessão. Quando o boleto for marcado como pago, o badge some da 1ª sessão.

Donald Winnicott · 4× R$ 200 = R$ 800

Ocorrências
4 semanais
Total
R$ 800,00
Como cobrar?
Estilo do pacote
Forma de pagamento

Financeiro · Donald

Clique em Salvar série

8 Recorrente · pacote único SALDO

Sem record financeiro inicial. Cria billing_contract com sessions_total. Records nascem 1 a 1 quando sessão é realizada.

Como testar: Criar série pra Carl Jung, 4 sessões R$ 40 = R$ 160 total, modo "Pacote único", estilo "Saldo". Salvar. Verificar ZERO record em /financeiro + 1 billing_contract com charging_style=saldo, sessions_total=4, sessions_used=0.
Indicadores visuais: Como não existe record financeiro inicial e o controle é por saldo do contrato, todas as 4 ocorrências ficam limpas no calendário (sem badge $). À medida que cada sessão for realizada, nasce 1 record por vez — e essa sessão recebe o badge $ até o record ser pago. Use "Antecipar pagamento" no popover quando o paciente quiser pagar uma sessão antes dela acontecer.

Carl Jung · 4× R$ 40 = R$ 160

Sessions total
4
Valor por sessão
R$ 40,00
Estilo do pacote

Cobra 1 record por sessão realizada. Pacote dá direito ao preço promocional + controle de saldo.

Financeiro · Carl Jung

Clique em Salvar série

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.

Como testar: Criar série pra Michael Balint, 12 sessões semanais R$ 150, modo "1 por sessão". Salvar. Verificar 12 records pending em /financeiro (1 por ocorrência) com agenda_evento_id apontando pra cada uma.
Indicadores visuais: Como cada ocorrência foi materializada com seu próprio record pendente, todas as 12 sessões mostram o badge $ no calendário e a linha "A receber R$ 150,00 (cobrança pendente)" no popover. À medida que o paciente pagar cada uma, o badge dessa sessão some.

Michael Balint · semanal

Frequência
Semanal · 12 sessões
Valor / sessão
R$ 150,00
Modo

Financeiro · Michael

Clique em Salvar série

10 Status change · avulsa pendente

Sessão criada com chargeMode=session, agora chegou a hora. Dialog confirm aparece em realizado/faltou/cancelado.

Como testar: Usar a sessão pendente do Henrique (cenário 3) ou criar uma nova pra Maria Magali R$ 200 cobrar ao salvar. (a) Mudar status → Realizada: dialog deve aparecer, confirmar, record continua pendente. (b) Reset (volta pra pendente). Mudar → Faltou: dialog aparece com multa editável R$ 100, confirmar, record original cancelado + novo de multa criado. (c) Reset. Mudar → Cancelado: dialog confirma cancelamento sem multa, record fica cancelado.

Maria Magali · 15/05 16:00 · pendente

Status atual
pendente
Valor sessão
R$ 200,00
Mudar status para:

Cada um abre o AgendaStatusChangeConfirmDialog com variante diferente.

Financeiro · Maria

Sessão 15/05 16:00
avulsa · particular
pendente
R$ 200,00

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.

Como testar: Usando o pacote do Carl Jung (cenário 8): (a) Marcar 1ª sessão como Realizada: dialog confirm → criar record R$ 40 pending + saldo cai pra 3/4. (b) Marcar 2ª como Faltou: dialog com 3 opções (consumir saldo / aplicar multa / nada). Testar a opção consumir saldo → saldo cai pra 2/4 sem cobrança nova. (c) Marcar 3ª como Faltou com aplicar multa R$ 20 → saldo continua 2/4, novo record R$ 20.

Carl Jung · pacote saldo

Saldo: 4 / 4
R$ 40/sessão
Sessão de hoje (15/05 18:00) — virtual rec::...::2026-05-15
pendente
Marcar como:
O que acontece no banco?
  • Materializa a ocorrência virtual: INSERT em agenda_eventos com recurrence_id + recurrence_date
  • UPDATE billing_contracts.sessions_used += 1
  • INSERT em financial_records R$ 40 status=pending (ou paid se já houver record do antecipar)

Financeiro · Carl Jung

Pacote 4× R$ 40
billing_contract · saldo 4/4
contrato
R$ 160,00
Ainda sem records de sessão. Cada Realizada cria 1 record + decrementa saldo.

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).

Como testar: No pacote do Carl Jung, clicar numa ocorrência virtual futura → popover abre → seção Financeiro mostra 2 botões: "Lançamentos" + "Antecipar pagamento". Clicar "Antecipar pagamento", escolher PIX (default), confirmar. (a) Verificar 1 novo record R$ 40 paid em /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

Seção Financeiro do popover:

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

Pacote 4× R$ 40
billing_contract · saldo 4/4
contrato
R$ 160,00

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).

Como testar: Pegar a sessão paga da Joyce (cenário 4) e abrir o 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

Esta sessão tem cobrança paga. Campos de valor / modalidade / tipo estão travados. Pra ajustar valor, use credit note no /financeiro.
Valor
R$ 200,00
Modalidade
Presencial

Apenas horário / observações podem ser editados.

Financeiro · paciente

Sessão 10/05 14:00
avulsa · particular · paid_at 10/05
pago
R$ 200,00
Record imutável. Reembolso parcial → credit note. Cancelamento → record novo negativo.