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) <noreply@anthropic.com>
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user