agenda pacote saldo: fix root cause + sequential awaits

ROOT CAUSE DESCOBERTO durante C11/A com Andre Green:
billing_contracts NAO tem coluna updated_at. UPDATEs em
_applyStatusDecisions passavam updated_at -> Postgres retornava
"column updated_at does not exist" -> Promise.allSettled engolia
como {status: 'rejected'} silencioso -> toast warn generico que
user nao percebia. Resultado: sessions_used nunca incrementava.

Bug existia em DOIS lugares:
1. consumeSaldo block (faltou/cancelado pacote saldo) - afetaria
   C11/B, C11/C, C11/D
2. generatePackageCharge block (realizado pacote saldo) - afetou
   C11/A

Em ambos: removido updated_at do patch (.update({...})).

ADICIONAL: generatePackageCharge refatorado pra usar AWAITS
SEQUENCIAIS (igual onUsarSessao do MelissaLayout que sempre
funcionou):
- 4a) UPDATE agenda_eventos.billing_contract_id (faltava!)
- 4b) RPC create_financial_record_for_session
- 4c) UPDATE billing_contracts.sessions_used + status=completed

Cada step com try/catch + console.error + toast distinto. Sem mais
falhas escondidas em Promise.allSettled paralelo.

Backfill manual do estado do Andre Green: evento 6e70476f agora
amarrado ao contract 691118da com sessions_used=1.

Memoria nova: project_billing_contracts_no_updated_at.md pra evitar
o gotcha no futuro.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-20 11:25:04 -03:00
parent 079509e001
commit 16dfa02bd1
@@ -1442,13 +1442,14 @@ function _buildHandlers(deps) {
const tasks = [];
// 1) Consumir saldo (pacote saldo + faltou/cancelado + decisão sim)
// ⚠ billing_contracts NÃO tem coluna updated_at — passar esse campo
// causa "column does not exist" silenciosamente em Promise.allSettled.
if (decision.consumeSaldo && ctx.billingContract?.id) {
tasks.push(
supabase
.from('billing_contracts')
.update({
sessions_used: (ctx.billingContract.sessions_used ?? 0) + 1,
updated_at: new Date().toISOString()
sessions_used: (ctx.billingContract.sessions_used ?? 0) + 1
})
.eq('id', ctx.billingContract.id)
);
@@ -1529,31 +1530,59 @@ function _buildHandlers(deps) {
);
}
// 4) Realizado em pacote saldo: cria cobrança individual + incrementa sessions_used
// 4) Realizado em pacote saldo: amarra contract + cria cobrança + incrementa saldo
// Refatorado pra usar AWAITS SEQUENCIAIS (igual onUsarSessao do MelissaLayout).
// Antes era Promise.allSettled paralelo que escondia falhas silenciosas
// — durante teste C11/A o sessions_used não incrementava + agenda_evento
// ficava sem billing_contract_id. Ambos os updates não rodavam mas o
// toast warn não aparecia. Agora cada step tem error explícito.
if (decision.generatePackageCharge && ctx.billingContract?.id) {
const amount = Number(row.price ?? 0);
const amount = Number(row.price ?? (ctx.billingContract.total_sessions > 0 ? (Number(ctx.billingContract.package_price) || 0) / ctx.billingContract.total_sessions : 0));
const dueIso = row.inicio_em ? new Date(row.inicio_em).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10);
// Cria record
tasks.push(
supabase.rpc('create_financial_record_for_session', {
// 4a) Amarra agenda_evento ao contrato. Pra virtual recém-materializada,
// _applyStatusUpdateOnly criou o evento SEM billing_contract_id —
// precisa update separado aqui.
try {
const { error: linkErr } = await supabase.from('agenda_eventos').update({ billing_contract_id: ctx.billingContract.id, updated_at: new Date().toISOString() }).eq('id', eventoId);
if (linkErr) throw linkErr;
} catch (e) {
console.error('[Fase5] erro amarrando billing_contract_id:', e?.message);
toast.add({ severity: 'warn', summary: 'Pacote — amarração', detail: e?.message || 'Falha ao amarrar sessão ao pacote', life: 5000 });
}
// 4b) Cria financial_record (RPC tolera idempotência)
try {
const { error: rpcErr } = await supabase.rpc('create_financial_record_for_session', {
p_tenant_id: tenantId,
p_owner_id: uid,
p_patient_id: patientId,
p_agenda_evento_id: eventoId,
p_amount: amount,
p_due_date: dueIso
})
);
// Incrementa saldo usado
tasks.push(
supabase
.from('billing_contracts')
.update({
sessions_used: (ctx.billingContract.sessions_used ?? 0) + 1,
updated_at: new Date().toISOString()
})
.eq('id', ctx.billingContract.id)
);
});
if (rpcErr) throw rpcErr;
} catch (e) {
console.error('[Fase5] erro RPC create_financial_record_for_session:', e?.message);
toast.add({ severity: 'error', summary: 'Cobrança', detail: e?.message || 'Falha ao gerar cobrança do pacote', life: 7000 });
}
// 4c) Incrementa sessions_used + completa contract se atingir total
// ⚠ billing_contracts NÃO tem coluna updated_at — passar esse
// campo causa "column does not exist" silenciosamente em
// Promise.allSettled (era o root cause do saldo não incrementar).
try {
const newUsed = (ctx.billingContract.sessions_used ?? 0) + 1;
const patchContract = { sessions_used: newUsed };
if (newUsed >= (ctx.billingContract.total_sessions ?? 0)) {
patchContract.status = 'completed';
}
const { error: incErr } = await supabase.from('billing_contracts').update(patchContract).eq('id', ctx.billingContract.id);
if (incErr) throw incErr;
} catch (e) {
console.error('[Fase5] erro incrementando sessions_used:', e?.message);
toast.add({ severity: 'warn', summary: 'Saldo do pacote', detail: e?.message || 'Falha ao atualizar saldo do pacote', life: 6000 });
}
}
// Roda tudo em paralelo (falha parcial é tolerável — toast warn)