/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Arquivo: src/features/financeiro/services/billingContractsRepository.js | | Repository de billing_contracts — pacotes/contratos de cobrança (upfront, | pagamento por sessão, etc). | | Gotcha conhecido (memória project_billing_contracts_no_updated_at): | billing_contracts NÃO tem coluna updated_at. UPDATE com updated_at falha | silently em Promise.allSettled. Repository NÃO inclui updated_at em patches. |-------------------------------------------------------------------------- */ import { supabase } from '@/lib/supabase/client'; import { useTenantStore } from '@/stores/tenantStore'; import { assertTenantId, getUid } from './_tenantGuards'; import { BILLING_CONTRACT_SELECT } from './financialSelects'; function resolveTenantId(tenantIdArg) { const tenantStore = useTenantStore(); const tenantId = tenantIdArg || tenantStore.activeTenantId || tenantStore.tenantId; assertTenantId(tenantId); return tenantId; } /** * Lista contratos ativos do paciente. */ export async function listForPatient(patientId, { tenantId, includeDeleted = false } = {}) { if (!patientId) return []; const tid = resolveTenantId(tenantId); let q = supabase.from('billing_contracts').select(BILLING_CONTRACT_SELECT).eq('tenant_id', tid).eq('patient_id', patientId).order('created_at', { ascending: false }); if (!includeDeleted) q = q.is('deleted_at', null); const { data, error } = await q; if (error) throw error; return data || []; } /** * Lê contrato por id. Refresh FRESH do banco — usado pelo orchestrator antes * de UPDATE pra evitar race condition (memória project_agenda_reverse_transitions). */ export async function getById(contractId, { tenantId } = {}) { if (!contractId) throw new Error('contractId obrigatório.'); const tid = resolveTenantId(tenantId); const { data, error } = await supabase.from('billing_contracts').select(BILLING_CONTRACT_SELECT).eq('id', contractId).eq('tenant_id', tid).maybeSingle(); if (error) throw error; return data || null; } /** * Cria contrato. */ export async function create(payload) { if (!payload?.patient_id) throw new Error('patient_id obrigatório.'); if (!payload?.charging_style) throw new Error('charging_style obrigatório.'); const uid = await getUid(); const tid = resolveTenantId(payload.tenantId); const row = { tenant_id: tid, owner_id: uid, patient_id: payload.patient_id, charging_style: payload.charging_style, sessions_total: payload.sessions_total ?? null, sessions_used: 0, total_amount: payload.total_amount != null ? Number(payload.total_amount) : null, status: payload.status || 'active', start_date: payload.start_date || null, end_date: payload.end_date || null }; const { data, error } = await supabase.from('billing_contracts').insert([row]).select(BILLING_CONTRACT_SELECT).single(); if (error) throw error; return data; } /** * Update — NÃO INCLUI updated_at (tabela não tem essa coluna — gotcha conhecido). */ export async function update(contractId, patch, { tenantId } = {}) { if (!contractId) throw new Error('contractId obrigatório.'); const tid = resolveTenantId(tenantId); // eslint-disable-next-line no-unused-vars const { updated_at: _dropped, ...safePatch } = patch || {}; const { data, error } = await supabase.from('billing_contracts').update(safePatch).eq('id', contractId).eq('tenant_id', tid).select(BILLING_CONTRACT_SELECT).single(); if (error) throw error; return data; } /** * Incrementa sessions_used em 1 (consume). * Lê FRESH antes do UPDATE pra evitar race. */ export async function incrementSessionsUsed(contractId, { tenantId } = {}) { const current = await getById(contractId, { tenantId }); if (!current) throw new Error('Contrato não encontrado.'); const newCount = (Number(current.sessions_used) || 0) + 1; return update(contractId, { sessions_used: newCount }, { tenantId }); } /** * Decrementa sessions_used (reverse). Lê FRESH antes. */ export async function decrementSessionsUsed(contractId, { tenantId } = {}) { const current = await getById(contractId, { tenantId }); if (!current) throw new Error('Contrato não encontrado.'); const newCount = Math.max(0, (Number(current.sessions_used) || 0) - 1); return update(contractId, { sessions_used: newCount }, { tenantId }); } /** * Busca records cross-week por recurrence_id (memória project_cross_week_propagation). * Útil pra bulk-load de pacote upfront. */ export async function findRecordsByRecurrence(recurrenceId, { tenantId } = {}) { if (!recurrenceId) return []; const tid = resolveTenantId(tenantId); const { data, error } = await supabase.from('financial_records').select('id, status, agenda_evento_id, billing_contract_id').eq('tenant_id', tid).is('deleted_at', null).not('agenda_evento_id', 'is', null); // NOTE: filter por recurrence_id requer join — fica como TODO no orchestrator // (memória project_cross_week_propagation: query records cross-week por recurrence_id). if (error) throw error; return data || []; }