/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Arquivo: src/features/financeiro/services/asaasGatewayService.js | | Cliente JS pra orquestrar cobranças Asaas via Edge Functions. | Browser NUNCA fala direto com Asaas — API key não pode chegar aqui. | | Arquitetura: ver development/02-auditoria/DESIGN_ASAAS_GATEWAY.md | | ⚠️ FOUNDATION SKELETON. Edge Functions ainda são stubs — chamadas vão | retornar erro até deploy real. Requer credenciais Asaas configuradas. | | Pré-requisitos (do user): | 1. Migration 20260521000001_asaas_gateway_tables.sql aplicada | 2. Migration 20260521000002_asaas_gateway_rls.sql aplicada | 3. Edge Functions deployadas (asaas-create-payment-record, asaas-cancel-payment) | 4. API key Asaas inserida em payment_settings (via UI futura ou SQL manual) | 5. payment_settings.asaas_enabled = true |-------------------------------------------------------------------------- */ import { supabase } from '@/lib/supabase/client'; import { useTenantStore } from '@/stores/tenantStore'; // ─── Status mapping Asaas → financial_records.status ──────────────────────── const ASAAS_TO_STATUS = { PENDING: 'pending', RECEIVED: 'paid', CONFIRMED: 'paid', RECEIVED_IN_CASH: 'paid', OVERDUE: 'overdue', REFUNDED: 'refunded', CHARGEBACK_REQUESTED: 'refunded', CHARGEBACK_DISPUTE: 'cancelled', DELETED: 'cancelled' }; export function mapAsaasStatus(asaasStatus) { return ASAAS_TO_STATUS[asaasStatus] || 'pending'; } // ─── Validations ──────────────────────────────────────────────────────────── function assertTenantId(tenantId) { if (!tenantId || tenantId === 'null' || tenantId === 'undefined') { throw new Error('Tenant ativo inválido pra operar Asaas.'); } } function resolveTenantId() { const tenantStore = useTenantStore(); const tid = tenantStore.activeTenantId || tenantStore.tenantId; assertTenantId(tid); return tid; } // ─── Core API ─────────────────────────────────────────────────────────────── /** * Cria cobrança Asaas pra um financial_record existente. * * Invoca Edge Function `asaas-create-payment-record` que: * 1. Lê financial_record + patient * 2. Garante asaas_customer existe (cascade pra create-customer-patient) * 3. POST /payments no Asaas com externalReference=financial_record.id * 4. INSERT asaas_payments + UPDATE financial_records.payment_link * * @param {string} financialRecordId * @param {Object} opts * @param {'PIX'|'BOLETO'|'CREDIT_CARD'} [opts.method='PIX'] * @param {string} [opts.dueDate] - YYYY-MM-DD. Default = financial_record.due_date * @returns {Promise<{asaas_payment_id, payment_url, pix_qr_code?, pix_copy_paste?, bank_slip_url?}>} */ export async function createPaymentForRecord(financialRecordId, opts = {}) { if (!financialRecordId) throw new Error('financialRecordId obrigatório.'); const tenantId = resolveTenantId(); const method = opts.method || 'PIX'; if (!['PIX', 'BOLETO', 'CREDIT_CARD'].includes(method)) { throw new Error(`Método inválido: ${method}. Aceitos: PIX, BOLETO, CREDIT_CARD.`); } const { data, error } = await supabase.functions.invoke('asaas-create-payment-record', { body: { tenant_id: tenantId, financial_record_id: financialRecordId, billing_type: method, due_date: opts.dueDate || null } }); if (error) throw new Error(formatEdgeError(error)); if (!data?.ok) throw new Error(data?.error || 'Falha ao criar cobrança Asaas.'); return data.payment; } /** * Cancela cobrança Asaas. Não afeta o financial_record diretamente — webhook * processará PAYMENT_DELETED e fará o sync. * * @param {string} asaasPaymentId */ export async function cancelPayment(asaasPaymentId) { if (!asaasPaymentId) throw new Error('asaasPaymentId obrigatório.'); const tenantId = resolveTenantId(); const { data, error } = await supabase.functions.invoke('asaas-cancel-payment', { body: { tenant_id: tenantId, asaas_payment_id: asaasPaymentId } }); if (error) throw new Error(formatEdgeError(error)); if (!data?.ok) throw new Error(data?.error || 'Falha ao cancelar cobrança.'); return data; } /** * Busca info de pagamento Asaas (PIX QR code, boleto URL, status atual). * Read-only: vai na tabela asaas_payments. Não chama API Asaas. * * @param {string} financialRecordId * @returns {Promise} */ export async function getPaymentForRecord(financialRecordId) { if (!financialRecordId) return null; const tenantId = resolveTenantId(); const { data, error } = await supabase .from('asaas_payments') .select('id, asaas_payment_id, billing_type, status, value, due_date, payment_date, invoice_url, payment_url, bank_slip_url, pix_qr_code, pix_copy_paste, cancelled_at') .eq('tenant_id', tenantId) .eq('financial_record_id', financialRecordId) .is('cancelled_at', null) .order('created_at', { ascending: false }) .limit(1) .maybeSingle(); if (error) throw error; return data || null; } /** * Sincroniza força um pagamento Asaas (consulta API Asaas + atualiza row local). * Use quando suspeitar que webhook falhou (record fica pending mas paciente diz que pagou). * * @param {string} asaasPaymentId */ export async function syncPayment(asaasPaymentId) { if (!asaasPaymentId) throw new Error('asaasPaymentId obrigatório.'); const tenantId = resolveTenantId(); const { data, error } = await supabase.functions.invoke('asaas-sync-payment', { body: { tenant_id: tenantId, asaas_payment_id: asaasPaymentId } }); if (error) throw new Error(formatEdgeError(error)); if (!data?.ok) throw new Error(data?.error || 'Falha ao sincronizar.'); return data; } /** * Verifica se gateway Asaas está habilitado pro tenant ativo. * Usado pra mostrar/esconder botões de cobrança Asaas na UI. */ export async function isGatewayEnabled() { const tenantId = resolveTenantId(); const { data, error } = await supabase.from('payment_settings').select('asaas_enabled, asaas_environment').eq('tenant_id', tenantId).maybeSingle(); if (error) return false; return !!data?.asaas_enabled; } // ─── Helpers ──────────────────────────────────────────────────────────────── function formatEdgeError(err) { if (typeof err === 'string') return err; if (err?.message) return err.message; if (err?.error) return String(err.error); return 'Erro desconhecido na Edge Function.'; }