de3898878a
DESIGN_ASAAS_GATEWAY.md documenta arquitetura. Schema novo: 2 migrations (tables + RLS) cobrindo asaas_customers + asaas_payments + asaas_webhook_events. Client service asaasGatewayService.js no features/financeiro. 3 Edge Function stubs (create-payment-record, cancel-payment, sync-payment) — webhook financial_records eh Fase B. Bloqueadores Fase B (implementacao real): user precisa criar conta Asaas, gerar API keys, configurar webhook, setar ENV vars no Supabase. Decisao modelo de negocio (A/B/C) tambem pendente. Stops marcados claramente no DESIGN. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
183 lines
7.0 KiB
JavaScript
183 lines
7.0 KiB
JavaScript
/*
|
|
|--------------------------------------------------------------------------
|
|
| 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<Object|null>}
|
|
*/
|
|
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.';
|
|
}
|