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:
@@ -1442,13 +1442,14 @@ function _buildHandlers(deps) {
|
|||||||
const tasks = [];
|
const tasks = [];
|
||||||
|
|
||||||
// 1) Consumir saldo (pacote saldo + faltou/cancelado + decisão sim)
|
// 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) {
|
if (decision.consumeSaldo && ctx.billingContract?.id) {
|
||||||
tasks.push(
|
tasks.push(
|
||||||
supabase
|
supabase
|
||||||
.from('billing_contracts')
|
.from('billing_contracts')
|
||||||
.update({
|
.update({
|
||||||
sessions_used: (ctx.billingContract.sessions_used ?? 0) + 1,
|
sessions_used: (ctx.billingContract.sessions_used ?? 0) + 1
|
||||||
updated_at: new Date().toISOString()
|
|
||||||
})
|
})
|
||||||
.eq('id', ctx.billingContract.id)
|
.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) {
|
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);
|
const dueIso = row.inicio_em ? new Date(row.inicio_em).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10);
|
||||||
// Cria record
|
|
||||||
tasks.push(
|
// 4a) Amarra agenda_evento ao contrato. Pra virtual recém-materializada,
|
||||||
supabase.rpc('create_financial_record_for_session', {
|
// _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_tenant_id: tenantId,
|
||||||
p_owner_id: uid,
|
p_owner_id: uid,
|
||||||
p_patient_id: patientId,
|
p_patient_id: patientId,
|
||||||
p_agenda_evento_id: eventoId,
|
p_agenda_evento_id: eventoId,
|
||||||
p_amount: amount,
|
p_amount: amount,
|
||||||
p_due_date: dueIso
|
p_due_date: dueIso
|
||||||
})
|
});
|
||||||
);
|
if (rpcErr) throw rpcErr;
|
||||||
// Incrementa saldo usado
|
} catch (e) {
|
||||||
tasks.push(
|
console.error('[Fase5] erro RPC create_financial_record_for_session:', e?.message);
|
||||||
supabase
|
toast.add({ severity: 'error', summary: 'Cobrança', detail: e?.message || 'Falha ao gerar cobrança do pacote', life: 7000 });
|
||||||
.from('billing_contracts')
|
}
|
||||||
.update({
|
|
||||||
sessions_used: (ctx.billingContract.sessions_used ?? 0) + 1,
|
// 4c) Incrementa sessions_used + completa contract se atingir total
|
||||||
updated_at: new Date().toISOString()
|
// ⚠ billing_contracts NÃO tem coluna updated_at — passar esse
|
||||||
})
|
// campo causa "column does not exist" silenciosamente em
|
||||||
.eq('id', ctx.billingContract.id)
|
// 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)
|
// Roda tudo em paralelo (falha parcial é tolerável — toast warn)
|
||||||
|
|||||||
Reference in New Issue
Block a user