From 00c4168393e5315dab7671e517f7123c56c085ad Mon Sep 17 00:00:00 2001 From: Leonardo Date: Wed, 20 May 2026 12:28:19 -0300 Subject: [PATCH] =?UTF-8?q?agenda:=20C12=20prep=20=E2=80=94=20detectar=20p?= =?UTF-8?q?aid=20pre-existente=20em=20pacote=20saldo=20realizada?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../AgendaStatusChangeConfirmDialog.vue | 27 ++++++++- .../melissa/composables/useMelissaAgenda.js | 60 +++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/features/agenda/components/AgendaStatusChangeConfirmDialog.vue b/src/features/agenda/components/AgendaStatusChangeConfirmDialog.vue index 36f14ae..266de3d 100644 --- a/src/features/agenda/components/AgendaStatusChangeConfirmDialog.vue +++ b/src/features/agenda/components/AgendaStatusChangeConfirmDialog.vue @@ -49,6 +49,9 @@ const props = defineProps({ billingContractStyle: { type: String, default: null }, // 'upfront' | 'saldo' | null // Quando avulsa+pendente e novoStatus='realizado': financial_record relacionado 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) sessionPrice: { type: Number, default: 0 }, // Reverse transition (novoStatus='agendado'): artefatos a desfazer. @@ -135,8 +138,11 @@ const showSaldoBlock = computed(() => isFaltouOrCancelado.value && isPacoteSaldo // Mostrar bloco "registrar pagamento": realizado + avulsa pendente const showRegistrarPagto = computed(() => isRealizado.value && isAvulsa.value && props.pendingRecord && props.pendingRecord.status === 'pending'); -// Mostrar bloco "cobrança no pacote": realizado + pacote saldo -const showCobrancaPacote = computed(() => isRealizado.value && isPacoteSaldo.value); +// Mostrar bloco "cobrança no pacote": realizado + pacote saldo + SEM paid pré-existente +// (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 ──────────────────────────────────────────────────────────── const headerTitle = computed(() => { @@ -443,6 +449,23 @@ function onCancel() { + +
+
+ + Sessão já paga via antecipação +
+
+ +
+ Cobrança de R$ {{ _fmtBRL(existingPaidRecord.final_amount || existingPaidRecord.amount) }} + já foi registrada como paga ({{ existingPaidRecord.payment_method || 'método não definido' }}). + Marcar como Realizada vai consumir 1 sessão do saldo + ({{ billingContract?.sessions_used ?? 0 }} → {{ (billingContract?.sessions_used ?? 0) + 1 }}/{{ billingContract?.total_sessions ?? '?' }}). +
+
+
+
diff --git a/src/layout/melissa/composables/useMelissaAgenda.js b/src/layout/melissa/composables/useMelissaAgenda.js index 9204c15..fcd1d8c 100644 --- a/src/layout/melissa/composables/useMelissaAgenda.js +++ b/src/layout/melissa/composables/useMelissaAgenda.js @@ -1243,6 +1243,7 @@ function _buildHandlers(deps) { billingContract: ctx.billingContract, billingContractStyle: ctx.billingContract?.charging_style ?? null, pendingRecord: ctx.pendingRecord, + existingPaidRecord: ctx.existingPaidRecord || null, sessionPrice: Number(row.price ?? 0), 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 // a desfazer — current status + ALL records ativos + saldo consumido. // 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 // Refatorado pra usar AWAITS SEQUENCIAIS (igual onUsarSessao do MelissaLayout). // Antes era Promise.allSettled paralelo que escondia falhas silenciosas