agenda: C12 prep — detectar paid pre-existente em pacote saldo realizada
Preparacao pra teste C12 (antecipar pagamento). Fluxo: 1. User clica "Antecipar pagamento" em virtual futura -> cria record paid R$ X sem consumir saldo 2. Depois marca a sessao como Realizada -> dialog deve detectar o paid + so consumir saldo (NAO criar record novo, evitar duplicidade) Sem esse fix, marcar Realizada apos antecipar abriria o dialog "Gerar cobranca?" com default true, gerando record novo duplicado. Implementacao: - _loadStatusChangeContext: carrega ctx.existingPaidRecord (qualquer paid linkado ao evento, n=1) - Dialog: nova prop existingPaidRecord + computed showAlreadyPaid (substitui showCobrancaPacote quando paid existe) - Template: bloco "Sessao ja paga via antecipacao" com info do pagamento + preview do consumo de saldo - _applyStatusDecisions: novo branch 4-pre roda ANTES do generatePackageCharge: se realizado+pacote saldo+paid existe, roda tasks pendentes (1b amarra) + incrementa saldo sem criar record. Return cedo. Backfill: Andre 10/06 voltou pra agendado + saldo 2/4 (estado limpo pra testar C12 com a sessao 10/06 antecipando). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,9 @@ const props = defineProps({
|
|||||||
billingContractStyle: { type: String, default: null }, // 'upfront' | 'saldo' | null
|
billingContractStyle: { type: String, default: null }, // 'upfront' | 'saldo' | null
|
||||||
// Quando avulsa+pendente e novoStatus='realizado': financial_record relacionado
|
// Quando avulsa+pendente e novoStatus='realizado': financial_record relacionado
|
||||||
pendingRecord: { type: Object, default: null },
|
pendingRecord: { type: Object, default: null },
|
||||||
|
// Quando pacote saldo + realizado + record paid pré-existente (C12 antecipado):
|
||||||
|
// dialog não oferece "Gerar cobrança" — só confirma consumo de saldo.
|
||||||
|
existingPaidRecord: { type: Object, default: null },
|
||||||
// Preço da sessão (pra calcular multa percentual e cobrança de pacote saldo)
|
// Preço da sessão (pra calcular multa percentual e cobrança de pacote saldo)
|
||||||
sessionPrice: { type: Number, default: 0 },
|
sessionPrice: { type: Number, default: 0 },
|
||||||
// Reverse transition (novoStatus='agendado'): artefatos a desfazer.
|
// Reverse transition (novoStatus='agendado'): artefatos a desfazer.
|
||||||
@@ -135,8 +138,11 @@ const showSaldoBlock = computed(() => isFaltouOrCancelado.value && isPacoteSaldo
|
|||||||
// Mostrar bloco "registrar pagamento": realizado + avulsa pendente
|
// Mostrar bloco "registrar pagamento": realizado + avulsa pendente
|
||||||
const showRegistrarPagto = computed(() => isRealizado.value && isAvulsa.value && props.pendingRecord && props.pendingRecord.status === 'pending');
|
const showRegistrarPagto = computed(() => isRealizado.value && isAvulsa.value && props.pendingRecord && props.pendingRecord.status === 'pending');
|
||||||
|
|
||||||
// Mostrar bloco "cobrança no pacote": realizado + pacote saldo
|
// Mostrar bloco "cobrança no pacote": realizado + pacote saldo + SEM paid pré-existente
|
||||||
const showCobrancaPacote = computed(() => isRealizado.value && isPacoteSaldo.value);
|
// (se já tem paid via antecipação, mostra o bloco "já pago" em vez deste)
|
||||||
|
const showCobrancaPacote = computed(() => isRealizado.value && isPacoteSaldo.value && !props.existingPaidRecord);
|
||||||
|
// Mostrar bloco "já paga via antecipação": realizado + pacote saldo + paid pré-existente
|
||||||
|
const showAlreadyPaid = computed(() => isRealizado.value && isPacoteSaldo.value && !!props.existingPaidRecord);
|
||||||
|
|
||||||
// ─── Header ────────────────────────────────────────────────────────────
|
// ─── Header ────────────────────────────────────────────────────────────
|
||||||
const headerTitle = computed(() => {
|
const headerTitle = computed(() => {
|
||||||
@@ -443,6 +449,23 @@ function onCancel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ─── Bloco "JÁ PAGA via antecipação" (C12 fluxo de retorno) ─── -->
|
||||||
|
<div v-if="showAlreadyPaid" class="asccd-block">
|
||||||
|
<div class="asccd-block__title">
|
||||||
|
<i class="pi pi-check-circle" />
|
||||||
|
Sessão já paga via antecipação
|
||||||
|
</div>
|
||||||
|
<div class="asccd-info">
|
||||||
|
<i class="pi pi-info-circle" />
|
||||||
|
<div>
|
||||||
|
Cobrança de <b>R$ {{ _fmtBRL(existingPaidRecord.final_amount || existingPaidRecord.amount) }}</b>
|
||||||
|
já foi registrada como paga ({{ existingPaidRecord.payment_method || 'método não definido' }}).
|
||||||
|
Marcar como Realizada vai <b>consumir 1 sessão do saldo</b>
|
||||||
|
({{ billingContract?.sessions_used ?? 0 }} → {{ (billingContract?.sessions_used ?? 0) + 1 }}/{{ billingContract?.total_sessions ?? '?' }}).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ─── Bloco COBRANÇA NO PACOTE (realizado + pacote saldo) ──── -->
|
<!-- ─── Bloco COBRANÇA NO PACOTE (realizado + pacote saldo) ──── -->
|
||||||
<div v-if="showCobrancaPacote" class="asccd-block">
|
<div v-if="showCobrancaPacote" class="asccd-block">
|
||||||
<div class="asccd-block__title">
|
<div class="asccd-block__title">
|
||||||
|
|||||||
@@ -1243,6 +1243,7 @@ function _buildHandlers(deps) {
|
|||||||
billingContract: ctx.billingContract,
|
billingContract: ctx.billingContract,
|
||||||
billingContractStyle: ctx.billingContract?.charging_style ?? null,
|
billingContractStyle: ctx.billingContract?.charging_style ?? null,
|
||||||
pendingRecord: ctx.pendingRecord,
|
pendingRecord: ctx.pendingRecord,
|
||||||
|
existingPaidRecord: ctx.existingPaidRecord || null,
|
||||||
sessionPrice: Number(row.price ?? 0),
|
sessionPrice: Number(row.price ?? 0),
|
||||||
reverseArtifacts: ctx.reverseArtifacts || null
|
reverseArtifacts: ctx.reverseArtifacts || null
|
||||||
});
|
});
|
||||||
@@ -1415,6 +1416,27 @@ function _buildHandlers(deps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3b) Paid record pré-existente (caso C12: antecipar pagamento).
|
||||||
|
// Quando user antecipou paga ANTES de marcar Realizada, o record paid
|
||||||
|
// já existe ao tempo do status change. Dialog precisa saber pra:
|
||||||
|
// - Não oferecer "Gerar cobrança nova" (geraria duplicidade)
|
||||||
|
// - Ainda incrementar sessions_used (a sessão consome saldo do pacote)
|
||||||
|
if (eventoId) {
|
||||||
|
try {
|
||||||
|
const { data } = await supabase
|
||||||
|
.from('financial_records')
|
||||||
|
.select('id, status, amount, final_amount, paid_at, payment_method')
|
||||||
|
.eq('agenda_evento_id', eventoId)
|
||||||
|
.eq('status', 'paid')
|
||||||
|
.order('paid_at', { ascending: false })
|
||||||
|
.limit(1)
|
||||||
|
.maybeSingle();
|
||||||
|
ctx.existingPaidRecord = data ?? null;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Fase5] erro existing paid record:', e?.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 4) Reverse transition (status novo='agendado'): carrega artefatos
|
// 4) Reverse transition (status novo='agendado'): carrega artefatos
|
||||||
// a desfazer — current status + ALL records ativos + saldo consumido.
|
// a desfazer — current status + ALL records ativos + saldo consumido.
|
||||||
// Sem isso, voltar pra agendado deixa multa/record/saldo órfão.
|
// Sem isso, voltar pra agendado deixa multa/record/saldo órfão.
|
||||||
@@ -1678,6 +1700,44 @@ function _buildHandlers(deps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4-pre) Realizado em pacote saldo + paid pré-existente (C12: antecipou)
|
||||||
|
// Sessão já paga via "Antecipar pagamento" anteriormente. Realizada
|
||||||
|
// agora não deve gerar record novo (duplicaria cobrança) — só
|
||||||
|
// amarrar contrato (via tasks 1b) + incrementar saldo. Rodamos os
|
||||||
|
// tasks pendentes antes do incremento pra não perder o link.
|
||||||
|
const hasAnticipatedPayment = novoStatus === 'realizado' && ctx.billingContract?.charging_style === 'saldo' && ctx.existingPaidRecord?.id;
|
||||||
|
if (hasAnticipatedPayment) {
|
||||||
|
// Roda tasks acumulados (basicamente 1b amarra) antes do incremento
|
||||||
|
if (tasks.length > 0) {
|
||||||
|
const results = await Promise.allSettled(tasks);
|
||||||
|
const failed = results.filter((r) => r.status === 'rejected');
|
||||||
|
if (failed.length > 0) {
|
||||||
|
console.warn('[Fase5/realizada-paid] tasks com falha:', failed.map((f) => f.reason?.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
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 newUsed = currentUsed + 1;
|
||||||
|
const patch = { sessions_used: newUsed };
|
||||||
|
if (newUsed >= (freshContract?.total_sessions ?? 0)) {
|
||||||
|
patch.status = 'completed';
|
||||||
|
}
|
||||||
|
const { error: incErr } = await supabase.from('billing_contracts').update(patch).eq('id', ctx.billingContract.id);
|
||||||
|
if (incErr) throw incErr;
|
||||||
|
toast.add({ severity: 'success', summary: 'Sessão consumida', detail: `Saldo: ${newUsed}/${freshContract?.total_sessions ?? '?'}. Pagamento já estava registrado.`, life: 4000 });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Fase5/realizada-paid] erro consumindo saldo:', e?.message);
|
||||||
|
toast.add({ severity: 'warn', summary: 'Saldo do pacote', detail: e?.message || 'Falha ao consumir saldo', life: 5000 });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 4) Realizado em pacote saldo: amarra contract + cria cobrança + incrementa saldo
|
// 4) Realizado em pacote saldo: amarra contract + cria cobrança + incrementa saldo
|
||||||
// Refatorado pra usar AWAITS SEQUENCIAIS (igual onUsarSessao do MelissaLayout).
|
// Refatorado pra usar AWAITS SEQUENCIAIS (igual onUsarSessao do MelissaLayout).
|
||||||
// Antes era Promise.allSettled paralelo que escondia falhas silenciosas
|
// Antes era Promise.allSettled paralelo que escondia falhas silenciosas
|
||||||
|
|||||||
Reference in New Issue
Block a user