/* |-------------------------------------------------------------------------- | Agência PSI — Edge Function: asaas-create-payment-record |-------------------------------------------------------------------------- | Cria cobrança Asaas pra um financial_record existente. | | ⚠️ STUB FOUNDATION — chamadas reais ao Asaas API marcadas TODO. | Fluxo completo descrito em DESIGN_ASAAS_GATEWAY.md §6. | | Input (body JSON): | { | tenant_id: uuid, | financial_record_id: uuid, | billing_type: 'PIX' | 'BOLETO' | 'CREDIT_CARD', | due_date?: 'YYYY-MM-DD' // default = financial_record.due_date | } | | Output (JSON): | { | ok: true, | payment: { | asaas_payment_id, payment_url, pix_qr_code?, pix_copy_paste?, bank_slip_url? | } | } | | Errors: | 400 — input inválido | 403 — gateway não habilitado pro tenant | 404 — record não encontrado | 500 — erro Asaas |-------------------------------------------------------------------------- */ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 'Access-Control-Allow-Methods': 'POST, OPTIONS' }; function json(body: unknown, status = 200) { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } Deno.serve(async (req: Request) => { if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders }); if (req.method !== 'POST') return json({ ok: false, error: 'method_not_allowed' }, 405); try { const body = (await req.json().catch(() => null)) as Record | null; if (!body) return json({ ok: false, error: 'invalid_body' }, 400); const tenantId = String(body.tenant_id || ''); const recordId = String(body.financial_record_id || ''); const billingType = String(body.billing_type || 'PIX'); const dueDateOverride = body.due_date ? String(body.due_date) : null; if (!tenantId || !recordId) return json({ ok: false, error: 'missing_fields' }, 400); if (!['PIX', 'BOLETO', 'CREDIT_CARD'].includes(billingType)) { return json({ ok: false, error: 'invalid_billing_type' }, 400); } const supa = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!); // 1. Verifica gateway habilitado + lê API key do tenant const { data: settings } = await supa .from('payment_settings') .select('asaas_enabled, asaas_environment, asaas_api_key_sandbox, asaas_api_key_prod') .eq('tenant_id', tenantId) .maybeSingle(); if (!settings?.asaas_enabled) { return json({ ok: false, error: 'gateway_not_enabled_for_tenant' }, 403); } const environment = settings.asaas_environment || 'sandbox'; const apiKey = environment === 'prod' ? settings.asaas_api_key_prod : settings.asaas_api_key_sandbox; const apiUrl = environment === 'prod' ? Deno.env.get('ASAAS_API_URL_PROD') || 'https://api.asaas.com/v3' : Deno.env.get('ASAAS_API_URL_SANDBOX') || 'https://sandbox.asaas.com/api/v3'; if (!apiKey) { return json({ ok: false, error: 'api_key_missing_for_environment', environment }, 403); } // 2. Lê financial_record + patient const { data: record } = await supa .from('financial_records') .select('id, tenant_id, patient_id, amount, due_date, description, status, deleted_at, agenda_evento_id') .eq('id', recordId) .eq('tenant_id', tenantId) .is('deleted_at', null) .maybeSingle(); if (!record) return json({ ok: false, error: 'record_not_found' }, 404); if (record.status !== 'pending') { return json({ ok: false, error: `record_not_pending (status=${record.status})` }, 409); } if (!record.patient_id) return json({ ok: false, error: 'record_has_no_patient' }, 400); const { data: patient } = await supa .from('patients') .select('id, nome_completo, email_principal, telefone, cpf') .eq('id', record.patient_id) .maybeSingle(); if (!patient) return json({ ok: false, error: 'patient_not_found' }, 404); if (!patient.cpf) return json({ ok: false, error: 'patient_missing_cpf_required_by_asaas' }, 400); // 3. Garante customer no Asaas (chama interna asaas-create-customer-patient OU inline) // TODO Fase B: chamar Edge Function asaas-create-customer-patient ou inline upsert. // Por ora, busca cache local — se não existe, retorna erro. let { data: customer } = await supa .from('asaas_customers') .select('id, asaas_customer_id') .eq('tenant_id', tenantId) .eq('patient_id', patient.id) .eq('environment', environment) .is('deleted_at', null) .maybeSingle(); if (!customer) { // TODO Fase B: criar customer via POST /customers no Asaas return json({ ok: false, error: 'customer_not_cached_TODO_create_via_asaas_api', hint: 'Implementar inline ou via chamada asaas-create-customer-patient' }, 501); } // 4. POST /payments no Asaas // TODO Fase B: fetch real à Asaas API const asaasPayload = { customer: customer.asaas_customer_id, billingType, value: Number(record.amount), dueDate: dueDateOverride || record.due_date, description: record.description || `Sessão #${record.id.slice(0, 8)}`, externalReference: record.id }; // TODO Fase B: substituir mock por fetch real // const asaasResp = await fetch(`${apiUrl}/payments`, { // method: 'POST', // headers: { 'Content-Type': 'application/json', 'access_token': apiKey }, // body: JSON.stringify(asaasPayload) // }); // const asaasPayment = await asaasResp.json(); return json({ ok: false, error: 'STUB_not_implemented_TODO_fase_B', note: 'Edge Function pronta na estrutura, fetch ao Asaas marcado TODO. Ver DESIGN_ASAAS_GATEWAY.md §9 (Phasing).', would_send_to_asaas: asaasPayload, api_url: apiUrl, environment }, 501); // 5. INSERT asaas_payments // TODO Fase B: salvar resposta + extras (QR code pra PIX) // 6. UPDATE financial_records.payment_link // TODO Fase B } catch (err) { console.error('[asaas-create-payment-record] fatal:', err); return json({ ok: false, error: String(err) }, 500); } });