agenda: C7 OK + Fase 6 lock-edit ativada em Melissa + cross-week payment propagation
Cenário 7 (Pacote UPFRONT — Ana Souza Ferreira 4×R$ 200 = R$ 800)
- Testado e passou. User criou Ana, pagou os R$ 800 em dinheiro pelo
Financeiro. Borda verde + popover "Pago R$ 800" funcionando.
Fase 6 (lock-edit cobrada) ativada em Melissa
- Removido guard `if (!props.occurrenceMode) return;` em
loadOccFinancialRecord (useAgendaEventLifecycle.js:217+). Agora ele
carrega em ambos modos (Rail/Clínica E Melissa)
- loadOccFinancialRecord SINTETIZA record paid/pending pra siblings de
contrato upfront ativo — assim TODAS as ocorrências da série mostram
"Cobrança paga R$ 800 do pacote" no AgendaEventDialog
- AgendaEventDialog card Sessão/Honorários (flow Melissa) ganhou lock
template: Tag em vez de Select billingType quando occFinancialRecord
existe; Message com cadeado "Cobrança de R$ X já emitida"
- AgendaEventoFinanceiroPanel só renderiza dentro do lock quando record
é REAL (não sintetizado) — evita "Gerar cobrança" indevido em sibling
- paymentSummary do Resumo lateral unificado pra usar occFinancialRecord
(em vez do sessionPaymentRecord paralelo de antes)
Cross-week propagation de pacote upfront
- BUG: ao navegar pra semana só com virtuais (sem reais), bulk-load
caía no else `_rulePaymentMap.value = {}` — virtuais perdiam estado
paid herdado
- FIX em useMelissaAgenda._reloadRange:
* Maps (payment/amount/rule) inicializados SEMPRE no início
* Propagação roda independente de realIds.length (depende só de
ruleIdsInView.size>0, considera reais E virtuais com recurrence_id)
* Query cross-week: pra cada rule em view, busca QUALQUER evento
sibling em qualquer semana + seus records pra determinar estado do
contrato. Encontra o record do pacote mesmo em outra semana
- Saldo NÃO propaga (filter: charging_style='upfront' || NULL); cada
sessão de saldo gera cobrança individual ao realizar
- Memória durável: memory/project_cross_week_propagation.md
Visualização de virtuais cobertas
- MelissaEventoPanel.showPaymentRow: virtuais só escondem quando state
='none'. Com paid/pending herdado, exibem linha colorida
- MelissaAgenda fcEvents: isPaidSession e badge $ pendente removeram
exigência de !is_occurrence. Virtuais herdadas via propagação mostram
borda verde / badge amber
Atalho "Gerar fatura" no popover
- Pill amber pequeno ao lado de "A cobrar R$ X" quando paymentVariant
='none' && !is_occurrence. Click → gerarCobrancaManual direto, fecha
popover pra impedir double-click. Tooltip: "Gerar fatura agora"
- Wire em MelissaLayout via novo emit gerar-cobranca + handler
onGerarCobrancaQuick
Info de pacote no popover
- Header agora mostra "Sessão · Pacote · N sessões" (computed
seriesLabel lê de _raw do rule)
Botão "Excluir série inteira"
- Novo emit delete-series em MelissaEventoPanel + botão ao lado de
"Excluir sessão" quando evento tem recurrence_id
- Handler onDeleteSeries em MelissaLayout: hard delete em 3 etapas
(financial_records pendentes → agenda_eventos materializados →
recurrence_rules CASCADE leva exceptions). Bloqueia se algum record
paid (estorno via Financeiro primeiro)
cancel_session some da agenda
- useRecurrence.expandRules agora pula occurrence com exception.type=
'cancel_session' (era visível com status cancelado; doc dizia que
some). patient_missed/therapist_canceled/holiday_block permanecem
como histórico
recurrence_exceptions cancel idempotente
- MelissaLayout onDeleteEvento usa upsert com onConflict pra exception
cancel — não quebra mais com unique violation em re-cancel
billing_contract_id na 1ª materializada
- _createPackageContract agora .select() o contrato após insert e seta
billing_contract_id no insert da 1ª agenda_eventos materializada
onVerLancamentos cobre virtual de upfront
- Antes virtual sempre toast "Sem lançamentos". Agora busca records via
siblings da série pra encontrar o do pacote. Saldo/sem pacote continua
com toast
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -395,13 +395,13 @@ const fcEvents = computed(() => {
|
||||
const pStatus = ev.paciente_status;
|
||||
const isInactivePatient = pStatus === 'Arquivado' || pStatus === 'Inativo';
|
||||
// Sessão paga → barra esquerda verde (override do border-left que o
|
||||
// FC pinta com a cor do commitment). Espelha as mesmas condições do
|
||||
// badge $ amber: sessão + paciente + não-virtual; aqui inverte pra
|
||||
// paymentState === 'paid'.
|
||||
// FC pinta com a cor do commitment). Aplica a virtuais TAMBÉM quando
|
||||
// herdam 'paid' de pacote upfront pago via propagação no bulk-load
|
||||
// (sem essa propagação, virtuais ficam paymentState='none' e o
|
||||
// check abaixo já as exclui).
|
||||
const isPaidSession =
|
||||
String(ev.tipo || '').toLowerCase() === 'sessao' &&
|
||||
(ev.patient_id || ev.paciente_id) &&
|
||||
!ev.is_occurrence &&
|
||||
ev.paymentState === 'paid';
|
||||
const cls = [];
|
||||
if (isInactivePatient) cls.push('ma-evt--inactive-patient');
|
||||
@@ -650,7 +650,16 @@ const fcOptions = computed(() => ({
|
||||
// não poluir séries recorrentes com pacote upfront/saldo (cobertas
|
||||
// pelo contrato, não por record-por-sessão).
|
||||
let payBadgeHtml = '';
|
||||
if (isSessao && ext.patient_id && !ext.is_occurrence && ext.paymentState !== 'paid') {
|
||||
// Badge $ amber pendente: aparece pra sessão com paciente quando há
|
||||
// cobrança pendente (paymentState='pending'/'overdue') OU quando é
|
||||
// REAL sem cobrança ainda ('none'). Virtuais com 'none' (saldo,
|
||||
// sem pacote, ou virtuais limpas) ficam SEM badge — só virtuais
|
||||
// herdando 'pending' de pacote upfront mostram o badge.
|
||||
const wantBadge = isSessao && ext.patient_id && ext.paymentState !== 'paid' && (
|
||||
ext.paymentState === 'pending' || ext.paymentState === 'overdue' ||
|
||||
(!ext.is_occurrence && (ext.paymentState === 'none' || !ext.paymentState))
|
||||
);
|
||||
if (wantBadge) {
|
||||
payBadgeHtml = `<span class="mc-fc-event__paybadge" title="Cobrança pendente"><i class="pi pi-dollar"></i></span>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,11 @@ const isSessaoComPaciente = computed(
|
||||
// — não polui séries com pacote upfront.
|
||||
const showPaymentRow = computed(() => {
|
||||
if (!isSessaoComPaciente.value) return false;
|
||||
if (ev.value.is_occurrence) return false;
|
||||
// Virtuais sem estado herdado de contrato ficam limpas (paymentState='none').
|
||||
// Quando herdam 'paid' ou 'pending' de pacote upfront via propagação no
|
||||
// bulk-load, exibem normalmente. Pacote saldo continua limpo (siblings
|
||||
// ficam 'none' propositadamente — modelo Cliniko).
|
||||
if (ev.value.is_occurrence && (!ev.value.paymentState || ev.value.paymentState === 'none')) return false;
|
||||
return !!ev.value.paymentState;
|
||||
});
|
||||
const paymentVariant = computed(() => {
|
||||
@@ -128,9 +132,13 @@ const paymentIcon = computed(() => {
|
||||
});
|
||||
const paymentLabel = computed(() => {
|
||||
const state = ev.value.paymentState;
|
||||
// Em sessão particular, valor mora em price. Em convênio, vai pra
|
||||
// insurance_value (price = null). Fallback cobre os dois casos.
|
||||
const valor = ev.value.price ?? ev.value.insurance_value;
|
||||
// Pra estado 'paid', usar o VALOR REAL pago (paymentAmount, vem do
|
||||
// financial_record). Em pacote upfront, é o package_price total —
|
||||
// o evento.price pode ter sido editado depois e divergir. Em outros
|
||||
// estados, fallback pro price/insurance_value do evento.
|
||||
const valor = state === 'paid' && ev.value.paymentAmount != null
|
||||
? ev.value.paymentAmount
|
||||
: (ev.value.price ?? ev.value.insurance_value);
|
||||
const valorFmt = (valor != null && !Number.isNaN(Number(valor)))
|
||||
? Number(valor).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
|
||||
: null;
|
||||
|
||||
@@ -881,31 +881,58 @@ async function confirmAnteciparPagamento() {
|
||||
async function onVerLancamentos() {
|
||||
const ev = eventoSelecionado.value;
|
||||
if (!ev?.id) return;
|
||||
// Ocorrência virtual ainda não foi materializada — id é sintético
|
||||
// `rec::<rule>::<date>`, não bate com agenda_evento_id (uuid).
|
||||
// Aborta sem query e avisa o user. 2026-05-14.
|
||||
// Ocorrência virtual: id é sintético `rec::<rule>::<date>`, não bate com
|
||||
// agenda_evento_id (uuid). Mas se a virtual pertence a uma série com
|
||||
// contrato upfront, os records do contrato (linkados a sibling
|
||||
// materializada) cobrem ela — busca via recurrence_id.
|
||||
const isVirtualId = typeof ev.id === 'string' && ev.id.startsWith('rec::');
|
||||
if (ev.is_occurrence || isVirtualId) {
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Sem lançamentos ainda',
|
||||
detail: 'Esta ocorrência ainda não foi materializada. Lançamentos aparecem após a primeira ação na sessão (status, edição etc).',
|
||||
life: 5000
|
||||
});
|
||||
return;
|
||||
}
|
||||
const isVirtual = ev.is_occurrence || isVirtualId;
|
||||
|
||||
lancamentosEventoTitulo.value = ev.pacienteNome || ev.label || ev.titulo || 'Sessão';
|
||||
lancamentosDialogOpen.value = true;
|
||||
lancamentosLoading.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id, description, amount, final_amount, status, due_date, paid_at, payment_method, created_at')
|
||||
.eq('agenda_evento_id', ev.id)
|
||||
.is('deleted_at', null)
|
||||
.order('created_at', { ascending: true });
|
||||
if (error) throw error;
|
||||
lancamentosList.value = data || [];
|
||||
let records = [];
|
||||
if (isVirtual && ev.recurrence_id) {
|
||||
// Pega records de QUALQUER sibling materializada (cobre o caso
|
||||
// pacote upfront onde só a 1ª tem record do pacote inteiro).
|
||||
const { data: siblings } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.select('id')
|
||||
.eq('recurrence_id', ev.recurrence_id);
|
||||
const ids = (siblings || []).map((s) => s.id);
|
||||
if (ids.length) {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id, description, amount, final_amount, status, due_date, paid_at, payment_method, created_at')
|
||||
.in('agenda_evento_id', ids)
|
||||
.is('deleted_at', null)
|
||||
.order('created_at', { ascending: true });
|
||||
if (error) throw error;
|
||||
records = data || [];
|
||||
}
|
||||
} else if (!isVirtual) {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id, description, amount, final_amount, status, due_date, paid_at, payment_method, created_at')
|
||||
.eq('agenda_evento_id', ev.id)
|
||||
.is('deleted_at', null)
|
||||
.order('created_at', { ascending: true });
|
||||
if (error) throw error;
|
||||
records = data || [];
|
||||
}
|
||||
lancamentosList.value = records;
|
||||
if (!records.length && isVirtual) {
|
||||
// Caso virtual sem records de contrato (saldo, sem pacote): fecha
|
||||
// o dialog e avisa que ainda não materializou (comportamento antigo).
|
||||
lancamentosDialogOpen.value = false;
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Sem lançamentos ainda',
|
||||
detail: 'Esta ocorrência ainda não foi materializada. Lançamentos aparecem após a primeira ação na sessão (status, edição etc).',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'warn', summary: 'Erro', detail: e?.message || 'Falha ao carregar lançamentos.', life: 4000 });
|
||||
lancamentosList.value = [];
|
||||
|
||||
@@ -117,7 +117,7 @@ function isoToDecimalHour(iso) {
|
||||
return d.getHours() + d.getMinutes() / 60;
|
||||
}
|
||||
|
||||
function normalizeForMelissa(r, paymentStateMap = null) {
|
||||
function normalizeForMelissa(r, paymentStateMap = null, paymentAmountMap = null, rulePaymentMap = null) {
|
||||
// r pode vir de useAgendaEvents (linha real do banco) ou de loadAndExpand
|
||||
// (ocorrência virtual com is_occurrence=true e id "rec::uuid::date").
|
||||
const isOccurrence = !!r.is_occurrence;
|
||||
@@ -128,8 +128,17 @@ function normalizeForMelissa(r, paymentStateMap = null) {
|
||||
// _reloadRange via bulk-query em financial_records). Default 'none'
|
||||
// pra ocorrências virtuais (sem id real ainda) e eventos sem record.
|
||||
// 'paid' | 'pending' | 'none'.
|
||||
const paymentState =
|
||||
(!isOccurrence && r.id && paymentStateMap && paymentStateMap[r.id]) || 'none';
|
||||
// Fallback pra virtuais ou reais sem record: herda do contrato upfront
|
||||
// pago da série via _rulePaymentMap (chave = recurrence_id).
|
||||
let paymentState =
|
||||
(!isOccurrence && r.id && paymentStateMap && paymentStateMap[r.id]) || null;
|
||||
let paymentAmount =
|
||||
!isOccurrence && r.id && paymentAmountMap ? (paymentAmountMap[r.id] ?? null) : null;
|
||||
if ((!paymentState || paymentState === 'none') && r.recurrence_id && rulePaymentMap && rulePaymentMap[r.recurrence_id]) {
|
||||
paymentState = rulePaymentMap[r.recurrence_id].state;
|
||||
if (paymentAmount == null) paymentAmount = rulePaymentMap[r.recurrence_id].amount;
|
||||
}
|
||||
if (!paymentState) paymentState = 'none';
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
@@ -151,6 +160,7 @@ function normalizeForMelissa(r, paymentStateMap = null) {
|
||||
recurrence_id: r.recurrence_id ?? null,
|
||||
recurrence_date: r.recurrence_date ?? r.original_date ?? null,
|
||||
paymentState,
|
||||
paymentAmount,
|
||||
price: r.price != null ? Number(r.price) : null,
|
||||
// insurance_value: pra convênio, o valor cobrado mora aqui (não em
|
||||
// price). Popover e Resumo usam fallback `price ?? insurance_value`
|
||||
@@ -333,9 +343,17 @@ export function useMelissaAgenda() {
|
||||
// badge "$ pendente" no FC e linha "A receber" no popover sem ter que
|
||||
// queryar por evento. 'paid' | 'pending'. Eventos não listados = 'none'.
|
||||
const _paymentStateMap = ref({});
|
||||
// Map evento_id → valor pago/cobrado (final_amount do record). Usado pelo
|
||||
// popover e Resumo do dialog quando paymentState='paid' pra mostrar o
|
||||
// valor REAL pago (vs evento.price que pode ter sido editado depois).
|
||||
const _paymentAmountMap = ref({});
|
||||
// Map recurrence_id → {state, amount}. Pra ocorrências virtuais (que não
|
||||
// têm id real e portanto não entram em _paymentStateMap), normalize lê
|
||||
// daqui o estado herdado do contrato upfront pago da série.
|
||||
const _rulePaymentMap = ref({});
|
||||
|
||||
// ── Eventos Melissa-normalizados (consumidos pelo MelissaAgenda FC) ──
|
||||
const eventos = computed(() => allRows.value.map((r) => normalizeForMelissa(r, _paymentStateMap.value)));
|
||||
const eventos = computed(() => allRows.value.map((r) => normalizeForMelissa(r, _paymentStateMap.value, _paymentAmountMap.value, _rulePaymentMap.value)));
|
||||
|
||||
// ── Eventos do FC original (se precisar — AgendaEventDialog quer
|
||||
// `allEvents` no shape FC pra checar conflitos) ─────────
|
||||
@@ -462,16 +480,21 @@ export function useMelissaAgenda() {
|
||||
// Não bloqueia render — eventos aparecem sem badge até este await
|
||||
// resolver (default 'none').
|
||||
const realIds = (rows.value || []).map((r) => r.id).filter(Boolean);
|
||||
// Inicializa maps SEMPRE (não condicional a realIds.length>0) — em
|
||||
// semanas onde só há virtuais (sem eventos reais), ainda precisamos
|
||||
// rodar a propagação cross-week pra ruleMap.
|
||||
const map = {};
|
||||
const amountMap = {};
|
||||
const ruleMap = {};
|
||||
// Filtra cancelados: cobrança cancelada não deve manter
|
||||
// paymentState='pending' (badge $ residual). Tratamos cancelled
|
||||
// como "sem cobrança ativa" → cai pro default 'none'.
|
||||
if (realIds.length) {
|
||||
// Filtra cancelados: cobrança cancelada não deve manter
|
||||
// paymentState='pending' (badge $ residual). Tratamos cancelled
|
||||
// como "sem cobrança ativa" → cai pro default 'none'.
|
||||
const { data: recs } = await supabase
|
||||
.from('financial_records')
|
||||
.select('agenda_evento_id, paid_at, status')
|
||||
.select('agenda_evento_id, paid_at, status, amount, final_amount')
|
||||
.in('agenda_evento_id', realIds)
|
||||
.neq('status', 'cancelled');
|
||||
const map = {};
|
||||
for (const id of realIds) map[id] = 'none';
|
||||
for (const rec of recs || []) {
|
||||
const eid = rec.agenda_evento_id;
|
||||
@@ -481,14 +504,147 @@ export function useMelissaAgenda() {
|
||||
// consideramos cobrança honrada. Filhos (multas/taxas) que
|
||||
// venham pendentes não revertem esse estado pro badge.
|
||||
map[eid] = 'paid';
|
||||
amountMap[eid] = rec.final_amount ?? rec.amount ?? null;
|
||||
} else if (map[eid] !== 'paid') {
|
||||
map[eid] = 'pending';
|
||||
if (amountMap[eid] == null) amountMap[eid] = rec.final_amount ?? rec.amount ?? null;
|
||||
}
|
||||
}
|
||||
_paymentStateMap.value = map;
|
||||
} else {
|
||||
_paymentStateMap.value = {};
|
||||
}
|
||||
|
||||
// Propagação de pacote UPFRONT — SEMPRE roda (mesmo sem eventos reais
|
||||
// na view), pra que virtuais isoladas em semanas futuras herdem o
|
||||
// estado do contrato cross-week. Antes estava dentro de
|
||||
// `if (realIds.length)` e falhava em semanas só com virtuais.
|
||||
//
|
||||
// Quando a 1ª materializada
|
||||
// de uma série tem record (paid OU pending), TODAS as ocorrências
|
||||
// do mesmo recurrence_id devem herdar o mesmo estado (cobertas
|
||||
// pelo contrato — cobrança única do pacote inteiro).
|
||||
// Funciona pra real rows; virtuais ficam por loadOccFinancialRecord
|
||||
// no occurrenceMode (não passam por este bulk).
|
||||
// Estratégia: pega rules dos eventos com record OU dos eventos
|
||||
// visíveis que têm recurrence_id (cross-week — record do pacote
|
||||
// pode estar em outra semana), busca contratos upfront ativos
|
||||
// pros pacientes, propaga estado pra siblings no view atual.
|
||||
try {
|
||||
// 1) Coleta rule_ids de TODOS os eventos visíveis que pertencem
|
||||
// a uma série. Antes só usávamos eventos COM record paid/pending,
|
||||
// o que falhava cross-week (record do pacote pode estar em
|
||||
// outra semana). Agora propagação cobre Ana 2/3/4 mesmo
|
||||
// quando o record está só no Ana 1.
|
||||
const ruleIdsInView = new Set();
|
||||
for (const r of rows.value || []) {
|
||||
if (r.recurrence_id) ruleIdsInView.add(r.recurrence_id);
|
||||
}
|
||||
// Virtuais expandidas (que vêm depois) também trazem rules:
|
||||
for (const r of _occurrenceRows.value || []) {
|
||||
if (r.recurrence_id) ruleIdsInView.add(r.recurrence_id);
|
||||
}
|
||||
if (ruleIdsInView.size) {
|
||||
// 2) Acha QUALQUER evento (em qualquer semana) das rules
|
||||
// em view + seus records paid/pending pra detectar o
|
||||
// estado do contrato.
|
||||
const { data: allRuleEvents } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.select('id, recurrence_id, patient_id')
|
||||
.in('recurrence_id', [...ruleIdsInView]);
|
||||
const ruleEventIds = (allRuleEvents || []).map((e) => e.id);
|
||||
let ruleRecords = [];
|
||||
if (ruleEventIds.length) {
|
||||
const { data: rr } = await supabase
|
||||
.from('financial_records')
|
||||
.select('agenda_evento_id, paid_at, status')
|
||||
.in('agenda_evento_id', ruleEventIds)
|
||||
.neq('status', 'cancelled');
|
||||
ruleRecords = rr || [];
|
||||
}
|
||||
// Mapeia: rule_id → state (paid > pending > none) + patient
|
||||
const evIdToRule = new Map();
|
||||
const evIdToPatient = new Map();
|
||||
for (const e of allRuleEvents || []) {
|
||||
evIdToRule.set(e.id, e.recurrence_id);
|
||||
evIdToPatient.set(e.id, e.patient_id);
|
||||
}
|
||||
const ruleToPatient = new Map();
|
||||
const ruleToState = new Map();
|
||||
for (const rec of ruleRecords) {
|
||||
const rid = evIdToRule.get(rec.agenda_evento_id);
|
||||
const pid = evIdToPatient.get(rec.agenda_evento_id);
|
||||
if (!rid || !pid) continue;
|
||||
ruleToPatient.set(rid, pid);
|
||||
const newState = rec.paid_at ? 'paid' : 'pending';
|
||||
const existing = ruleToState.get(rid);
|
||||
if (existing !== 'paid') ruleToState.set(rid, newState);
|
||||
}
|
||||
if (ruleToPatient.size) {
|
||||
// Confirma contratos upfront pros pacientes envolvidos
|
||||
const patientIds = [...new Set(ruleToPatient.values())];
|
||||
const { data: contracts } = await supabase
|
||||
.from('billing_contracts')
|
||||
.select('patient_id, charging_style, status, type')
|
||||
.in('patient_id', patientIds);
|
||||
const activePackages = (contracts || []).filter((c) => c.type === 'package' && c.status === 'active');
|
||||
// NULL charging_style → assume upfront (default histórico
|
||||
// antes da migration 20260514000003). Pra dados antigos
|
||||
// sem a coluna preenchida, evita virtuais ficarem sem
|
||||
// propagação. 'saldo' explícito mantém siblings 'none'.
|
||||
const upfrontPatients = new Set(
|
||||
activePackages
|
||||
.filter((c) => c.charging_style === 'upfront' || c.charging_style == null)
|
||||
.map((c) => c.patient_id)
|
||||
);
|
||||
const ruleIdsToPropagate = [...ruleToPatient.entries()]
|
||||
.filter(([, pid]) => upfrontPatients.has(pid))
|
||||
.map(([rid]) => rid);
|
||||
if (ruleIdsToPropagate.length) {
|
||||
const { data: siblings } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.select('id, recurrence_id')
|
||||
.in('recurrence_id', ruleIdsToPropagate);
|
||||
// Captura o package_price do contrato pra propagar
|
||||
// valor real pra siblings (não o per-session que
|
||||
// pode ter sido editado depois)
|
||||
const contractPriceByPatient = new Map();
|
||||
const { data: contractsDetail } = await supabase
|
||||
.from('billing_contracts')
|
||||
.select('patient_id, package_price')
|
||||
.in('patient_id', [...upfrontPatients])
|
||||
.eq('charging_style', 'upfront')
|
||||
.eq('status', 'active');
|
||||
for (const c of contractsDetail || []) {
|
||||
contractPriceByPatient.set(c.patient_id, c.package_price);
|
||||
}
|
||||
for (const s of siblings || []) {
|
||||
if (map[s.id] !== undefined) {
|
||||
const stateToPropagate = ruleToState.get(s.recurrence_id) || 'pending';
|
||||
// Não rebaixa: se sibling já está paid
|
||||
// (record próprio paid), mantém.
|
||||
if (map[s.id] !== 'paid') map[s.id] = stateToPropagate;
|
||||
const pid = ruleToPatient.get(s.recurrence_id);
|
||||
const price = contractPriceByPatient.get(pid);
|
||||
if (price != null) amountMap[s.id] = price;
|
||||
}
|
||||
}
|
||||
// Populate _rulePaymentMap pra que VIRTUAIS (ainda
|
||||
// não materializadas) também herdem o estado ao
|
||||
// passar pelo normalize.
|
||||
for (const rid of ruleIdsToPropagate) {
|
||||
const pid = ruleToPatient.get(rid);
|
||||
const price = contractPriceByPatient.get(pid);
|
||||
const state = ruleToState.get(rid) || 'pending';
|
||||
ruleMap[rid] = { state, amount: price ?? null };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[useMelissaAgenda] propagação upfront falhou:', e?.message);
|
||||
}
|
||||
|
||||
_paymentStateMap.value = map;
|
||||
_paymentAmountMap.value = amountMap;
|
||||
_rulePaymentMap.value = ruleMap;
|
||||
}
|
||||
|
||||
async function refetch() {
|
||||
@@ -2130,18 +2286,23 @@ async function _createPackageContract({ rule, normalized, recorrencia, tenantId,
|
||||
// charging_style: identifica como o pacote foi cobrado na criação;
|
||||
// handler de status change usa pra decidir entre "só status" (upfront)
|
||||
// ou "criar cobrança + consumir saldo" (saldo).
|
||||
const { error: contractErr } = await supabase.from('billing_contracts').insert({
|
||||
owner_id: normalized.owner_id,
|
||||
tenant_id: tenantId,
|
||||
patient_id: normalized.paciente_id,
|
||||
type: 'package',
|
||||
total_sessions: n,
|
||||
sessions_used: 0,
|
||||
package_price: packagePrice,
|
||||
status: 'active',
|
||||
charging_style: packageStyle === 'saldo' ? 'saldo' : 'upfront'
|
||||
});
|
||||
const { data: createdContract, error: contractErr } = await supabase
|
||||
.from('billing_contracts')
|
||||
.insert({
|
||||
owner_id: normalized.owner_id,
|
||||
tenant_id: tenantId,
|
||||
patient_id: normalized.paciente_id,
|
||||
type: 'package',
|
||||
total_sessions: n,
|
||||
sessions_used: 0,
|
||||
package_price: packagePrice,
|
||||
status: 'active',
|
||||
charging_style: packageStyle === 'saldo' ? 'saldo' : 'upfront'
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
if (contractErr) throw contractErr;
|
||||
const contractId = createdContract?.id ?? null;
|
||||
|
||||
// Estilo 'saldo': para aqui — sem cobrança imediata.
|
||||
if (packageStyle === 'saldo') {
|
||||
@@ -2180,6 +2341,7 @@ async function _createPackageContract({ rule, normalized, recorrencia, tenantId,
|
||||
determined_commitment_id: normalized.determined_commitment_id ?? null,
|
||||
modalidade: rule.modalidade || normalized.modalidade || 'presencial',
|
||||
price: packagePrice,
|
||||
billing_contract_id: contractId,
|
||||
visibility_scope: normalized.visibility_scope || 'public'
|
||||
})
|
||||
.select('id')
|
||||
|
||||
Reference in New Issue
Block a user