From d6423da9b457626f2f3e30aef56cc48b37e0b637 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Wed, 20 May 2026 08:27:16 -0300 Subject: [PATCH] agenda: pre-C10 fix _applyStatusDecisions cancela pendingRecord Bug: ao mudar status pra faltou/cancelado com multa configurada em financial_exceptions, _applyStatusDecisions INSERIA o novo record da multa MAS deixava o pendingRecord original em pending. Resultado: cobranca dupla (R$ 200 original + R$ 30 multa = R$ 230). Fix em useMelissaAgenda.js:1450-1505: - applyFine agora carrega data da sessao na description ("Multa por falta - sessao dd/mm/aa") pro paciente identificar na fatura. - Novo bloco 2b: cancela ctx.pendingRecord quando faltou/cancelado, com nota de auditoria appendada em notes ("[YYYY-MM-DD] Cancelada - substituida por multa de no-show" ou similar). Vale tanto pra caso com multa quanto sem (status mudado sem aplicar multa). Fix dormente em useAgendaFinanceiro.js:59 ('fixed' -> 'fixed_fee') - charge_mode no schema eh 'fixed_fee' mas calcChargeAmount usava 'fixed' silenciosamente caia no fallback. Path nao exercitado na Melissa (usa _applyStatusDecisions, nao handleStatusChange), mas iria quebrar se algum dia fosse. Pre-teste C10: financial_exceptions seedadas no DB para tenant Bruno Terapeuta / owner Leonardo: - patient_no_show: fixed_fee R$ 30 - patient_cancellation: full, min_hours_notice=2, default_consume_on_miss=true Co-Authored-By: Claude Opus 4.7 (1M context) --- HANDOFF.md | 33 ++++++++++------- src/composables/useAgendaFinanceiro.js | 2 +- .../melissa/composables/useMelissaAgenda.js | 36 +++++++++++++++++-- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 7131306..18cf795 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1,19 +1,28 @@ -# HANDOFF — 2026-05-20 madrugada (C1-C9 ✅, próximo C10) +# HANDOFF — 2026-05-20 (C10 pré-teste · code-fix + seeds prontos) Documento de continuidade. **Quando voltar, comece lendo esta página até o fim.** -> **🎯 SE A FORÇA CAIR / SESSÃO PERDER CONTEXTO:** estamos na rodada de -> testes manuais dos 13 cenários do doc viva -> `src/docs/agenda-compromisso-financeiro-cenarios.html`. **C1-C9 ✅**. -> Próximo: **Cenário 10** (status change avulsa — Joyce/Ana/Sándor: -> marcar como realizado/faltou/cancelado e ver consequências no -> financeiro via STATUS_TO_EXCEPTION + financial_exceptions). +> **🎯 SE A FORÇA CAIR / SESSÃO PERDER CONTEXTO:** estamos no **Cenário 10** +> (status change avulsa). Code-fix pra cancelar pendingRecord aplicado + +> financial_exceptions da Melissa seedadas no DB. **Falta**: rodar os 5 +> sub-testes (a/a2/b/c/c2) e validar. -> **🟢 WORKING TREE LIMPO** após commit/push do checkpoint pós-C8/C9. -> Per-session funcionando (12 events + 12 records gerados em batch). -> Financeiro com rowGroup por paciente (expand/collapse). Bubble-up -> @cobranca-atualizada → M.refetch faz o card da agenda atualizar -> badge/borda imediatamente após pagar. +> **🟢 PRÉ-TESTE C10 SALVO**. Pós-teste, próximos cenários: C11 (status +> change pacote saldo) → C12 (antecipar pagamento) → C13 (edit cobrada). + +### Code-fix aplicado em 20/05 (pré-C10) +- **`useMelissaAgenda.js:1450-1505`** — `_applyStatusDecisions` agora cancela + o `ctx.pendingRecord` quando faltou/cancelado (com ou sem multa). Antes + inseria a multa mas DEIXAVA o original pending → cobrança dupla + (R$ 200 + R$ 30 = R$ 230). Audit trail vai em `notes` do record + cancelado, descrição da multa nova carrega data: "Multa por falta · sessão 20/05/26". +- **`useAgendaFinanceiro.js:59`** — fix dormente `'fixed'` → `'fixed_fee'` + (off-by-key contra schema; path nunca exercitado na Melissa, mas iria + quebrar se algum dia fosse). + +### Financial exceptions seedadas (tenant Bruno Terapeuta / owner Leonardo) +- `patient_no_show` → `fixed_fee R$ 30` +- `patient_cancellation` → `full`, `min_hours_notice=2`, `default_consume_on_miss=true` --- diff --git a/src/composables/useAgendaFinanceiro.js b/src/composables/useAgendaFinanceiro.js index 6dca753..46aef5f 100644 --- a/src/composables/useAgendaFinanceiro.js +++ b/src/composables/useAgendaFinanceiro.js @@ -56,7 +56,7 @@ const STATUS_TO_EXCEPTION = { function calcChargeAmount(originalAmount, rule) { if (!rule || rule.charge_mode === 'none') return 0; if (rule.charge_mode === 'full') return originalAmount; - if (rule.charge_mode === 'fixed') return rule.charge_value ?? 0; + if (rule.charge_mode === 'fixed_fee') return rule.charge_value ?? 0; if (rule.charge_mode === 'percentage') { const pct = rule.charge_pct ?? 0; return parseFloat(((originalAmount * pct) / 100).toFixed(2)); diff --git a/src/layout/melissa/composables/useMelissaAgenda.js b/src/layout/melissa/composables/useMelissaAgenda.js index 06f0889..eb1ea6c 100644 --- a/src/layout/melissa/composables/useMelissaAgenda.js +++ b/src/layout/melissa/composables/useMelissaAgenda.js @@ -1447,9 +1447,12 @@ function _buildHandlers(deps) { ); } - // 2) Aplicar multa (cria financial_record avulsa) + // 2) Aplicar multa (cria financial_record avulsa). Description leva + // data da sessão pra paciente identificar na fatura mesmo após cancel. if (decision.applyFine && decision.fineAmount > 0) { const dueIso = row.inicio_em ? new Date(row.inicio_em).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10); + const sessaoLabel = row.inicio_em ? new Date(row.inicio_em).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit' }) : ''; + const fineDesc = novoStatus === 'faltou' ? `Multa por falta · sessão ${sessaoLabel}` : `Taxa de cancelamento tardio · sessão ${sessaoLabel}`; const finePayload = { owner_id: uid, tenant_id: tenantId, @@ -1457,7 +1460,7 @@ function _buildHandlers(deps) { agenda_evento_id: eventoId, amount: decision.fineAmount, final_amount: decision.fineAmount, - description: novoStatus === 'faltou' ? 'Multa por falta (no-show)' : 'Taxa de cancelamento', + description: fineDesc.trim(), status: 'pending', due_date: dueIso, type: 'receita' @@ -1475,6 +1478,35 @@ function _buildHandlers(deps) { ); } + // 2b) Cancelar cobrança original (faltou/cancelado + pendingRecord). + // A sessão não aconteceu/foi cancelada → original substituída pela + // multa (se aplicada) ou simplesmente cancelada. Sem isso cobrava + // dobrado: original R$200 pending + multa R$30 = R$230. Audit trail + // preserva original em notes. + const isFaltouOuCancelado = novoStatus === 'faltou' || novoStatus === 'cancelado'; + if (isFaltouOuCancelado && ctx.pendingRecord?.id) { + const reasonText = decision.applyFine + ? novoStatus === 'faltou' + ? 'Cancelada — substituída por multa de no-show' + : 'Cancelada — substituída por taxa de cancelamento tardio' + : novoStatus === 'faltou' + ? 'Cancelada — sessão não realizada (paciente faltou)' + : 'Cancelada — sessão cancelada'; + const today = new Date().toISOString().slice(0, 10); + const noteEntry = `[${today}] ${reasonText}`; + const noteText = ctx.pendingRecord.notes ? `${ctx.pendingRecord.notes}\n${noteEntry}` : noteEntry; + tasks.push( + supabase + .from('financial_records') + .update({ + status: 'cancelled', + notes: noteText, + updated_at: new Date().toISOString() + }) + .eq('id', ctx.pendingRecord.id) + ); + } + // 3) Realizado avulsa pendente: marcar pendingRecord como pago (ou só status) if (decision.markPaid && ctx.pendingRecord?.id) { tasks.push(