agenda: link universal pacote + refresh saldo no reverse
Dois bugs descobertos durante C11/C+D: 1) Faltou+multa SEM consumeSaldo nao amarrava billing_contract_id no agenda_evento (so amarrava se consumeSaldo=true). Resultado: sessao 27/05 do Andre faltou+multa-sem-consume ficou sem rastro do contrato no DB. Reverse posterior nao detectaria saldoConsumed. Fix: bloco 1b) universal — sempre amarra quando forward (realizado/ faltou/cancelado) + tem contract + eventoId. Cobre todos os combos (multa-sem-consume, multa-com-consume, generatePackageCharge, consumeSaldo solo). 2) Reverse decrementar saldo as vezes nao persistia. Suspeita: race com ctx.billingContract.sessions_used stale do _loadStatusChangeContext quando flows rapidos sequenciais (Realizada+gerar -> Agendada imediato). Fix: refetch FRESH do billing_contracts.sessions_used direto do DB ANTES de calcular newUsed. Mais robusto contra qualquer race condition. Adicionado console.log pra futura debug. Removida duplicidade do amarra-billing_contract_id no bloco consumeSaldo (universal cobre). Backfill Andre Green: 27/05 amarrado, saldo voltou pra 2/4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1529,14 +1529,26 @@ function _buildHandlers(deps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2) Devolver saldo ao pacote (se decidiu)
|
// 2) Devolver saldo ao pacote (se decidiu)
|
||||||
|
// Refetch sessions_used FRESH antes de decrementar pra evitar
|
||||||
|
// race condition com flows que rodaram entre _loadStatusChangeContext
|
||||||
|
// e este ponto (ex: Realizada+gerar imediatamente seguido de Agendada).
|
||||||
if (decision.reverseRestoreSaldo && r.saldoConsumed && ctx.billingContract?.id) {
|
if (decision.reverseRestoreSaldo && r.saldoConsumed && ctx.billingContract?.id) {
|
||||||
try {
|
try {
|
||||||
const newUsed = Math.max(0, (ctx.billingContract.sessions_used ?? 0) - 1);
|
const { data: freshContract, error: fetchErr } = await supabase
|
||||||
|
.from('billing_contracts')
|
||||||
|
.select('sessions_used, total_sessions, status')
|
||||||
|
.eq('id', ctx.billingContract.id)
|
||||||
|
.maybeSingle();
|
||||||
|
if (fetchErr) throw fetchErr;
|
||||||
|
const currentUsed = freshContract?.sessions_used ?? 0;
|
||||||
|
const totalSessions = freshContract?.total_sessions ?? 0;
|
||||||
|
const newUsed = Math.max(0, currentUsed - 1);
|
||||||
const patch = { sessions_used: newUsed };
|
const patch = { sessions_used: newUsed };
|
||||||
// Se contrato estava 'completed' (atingiu total) e voltou abaixo, reativa.
|
// Se contrato estava 'completed' (atingiu total) e voltou abaixo, reativa.
|
||||||
if ((ctx.billingContract.sessions_used ?? 0) >= (ctx.billingContract.total_sessions ?? 0)) {
|
if (currentUsed >= totalSessions) {
|
||||||
patch.status = 'active';
|
patch.status = 'active';
|
||||||
}
|
}
|
||||||
|
console.log('[Fase5/reverse] decrementando saldo:', { from: currentUsed, to: newUsed, contractId: ctx.billingContract.id });
|
||||||
const { error: dErr } = await supabase.from('billing_contracts').update(patch).eq('id', ctx.billingContract.id);
|
const { error: dErr } = await supabase.from('billing_contracts').update(patch).eq('id', ctx.billingContract.id);
|
||||||
if (dErr) throw dErr;
|
if (dErr) throw dErr;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1564,9 +1576,7 @@ function _buildHandlers(deps) {
|
|||||||
// 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
|
// ⚠ billing_contracts NÃO tem coluna updated_at — passar esse campo
|
||||||
// causa "column does not exist" silenciosamente em Promise.allSettled.
|
// causa "column does not exist" silenciosamente em Promise.allSettled.
|
||||||
// Também precisa amarrar billing_contract_id no evento — sem isso, o
|
// Amarração de billing_contract_id no evento é feita em 1b) universal.
|
||||||
// reverse não detecta saldoConsumed depois (bug cascata descoberto
|
|
||||||
// durante teste C11/B: Falta+Descontar, depois Agendada não devolvia).
|
|
||||||
if (decision.consumeSaldo && ctx.billingContract?.id) {
|
if (decision.consumeSaldo && ctx.billingContract?.id) {
|
||||||
tasks.push(
|
tasks.push(
|
||||||
supabase
|
supabase
|
||||||
@@ -1576,7 +1586,15 @@ function _buildHandlers(deps) {
|
|||||||
})
|
})
|
||||||
.eq('id', ctx.billingContract.id)
|
.eq('id', ctx.billingContract.id)
|
||||||
);
|
);
|
||||||
// Amarra evento ao contrato pra rastreabilidade + reverse correto
|
}
|
||||||
|
|
||||||
|
// 1b) Amarra evento ao contrato — universal pra forward em pacote.
|
||||||
|
// Antes só rodava em consumeSaldo / generatePackageCharge. Faltou+multa
|
||||||
|
// SEM consume era exceção: evento ficava sem billing_contract_id,
|
||||||
|
// impedindo o reverse de detectar o vínculo depois. Fix: amarrar
|
||||||
|
// sempre que há contract envolvido + status forward + eventoId real.
|
||||||
|
const isForwardStatus = novoStatus === 'realizado' || novoStatus === 'faltou' || novoStatus === 'cancelado';
|
||||||
|
if (isForwardStatus && ctx.billingContract?.id && eventoId) {
|
||||||
tasks.push(
|
tasks.push(
|
||||||
supabase
|
supabase
|
||||||
.from('agenda_eventos')
|
.from('agenda_eventos')
|
||||||
|
|||||||
Reference in New Issue
Block a user