asaas: Tier 1 Fase A foundation — migrations + service + edge function stubs

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>
This commit is contained in:
Leonardo
2026-05-21 04:20:52 -03:00
parent ee2967a075
commit de3898878a
7 changed files with 1043 additions and 0 deletions
@@ -0,0 +1,138 @@
-- ============================================================================
-- Asaas Gateway — Tier 1 (cobrança de paciente) — schema foundation
-- ----------------------------------------------------------------------------
-- Cria 3 tabelas novas + adiciona 4 colunas em payment_settings.
-- Schema preparado pra Fase 3 do ROADMAP (gateway de pagamento).
--
-- ⚠️ Não habilita o gateway sozinho. Requer:
-- - Edge Functions deployadas
-- - API keys configuradas em payment_settings
-- - Webhook setado no dashboard Asaas
--
-- Ver: development/02-auditoria/DESIGN_ASAAS_GATEWAY.md
-- ============================================================================
BEGIN;
-- ──────────────────────────────────────────────────────────────────────────
-- 1. asaas_customers — mapping patient ↔ Asaas customer
-- ──────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS public.asaas_customers (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL,
patient_id uuid NOT NULL REFERENCES public.patients(id) ON DELETE CASCADE,
asaas_customer_id text NOT NULL,
environment text NOT NULL DEFAULT 'sandbox' CHECK (environment IN ('sandbox', 'prod')),
-- dados cacheados (sincronizados quando atualizar patient)
name text NOT NULL,
email text,
cpf_cnpj text,
phone text,
address jsonb,
created_at timestamptz DEFAULT now() NOT NULL,
updated_at timestamptz DEFAULT now() NOT NULL,
deleted_at timestamptz,
CONSTRAINT asaas_customers_unique_per_env UNIQUE (tenant_id, patient_id, environment)
);
COMMENT ON TABLE public.asaas_customers IS
'Mapping de pacientes para Asaas customers (1:1 por environment). Cacheado pra evitar re-criação a cada cobrança.';
CREATE INDEX IF NOT EXISTS idx_asaas_customers_lookup
ON public.asaas_customers (tenant_id, patient_id)
WHERE deleted_at IS NULL;
-- ──────────────────────────────────────────────────────────────────────────
-- 2. asaas_payments — 1 row por cobrança gerada
-- ──────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS public.asaas_payments (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL,
financial_record_id uuid NOT NULL REFERENCES public.financial_records(id) ON DELETE CASCADE,
asaas_customer_id uuid REFERENCES public.asaas_customers(id),
asaas_payment_id text NOT NULL,
asaas_invoice_id text,
environment text NOT NULL DEFAULT 'sandbox' CHECK (environment IN ('sandbox', 'prod')),
billing_type text NOT NULL CHECK (billing_type IN ('PIX', 'BOLETO', 'CREDIT_CARD', 'UNDEFINED')),
status text NOT NULL,
value numeric(10, 2) NOT NULL,
net_value numeric(10, 2),
due_date date NOT NULL,
payment_date timestamptz,
invoice_url text,
payment_url text,
bank_slip_url text,
pix_qr_code text,
pix_copy_paste text,
created_at timestamptz DEFAULT now() NOT NULL,
updated_at timestamptz DEFAULT now() NOT NULL,
cancelled_at timestamptz,
CONSTRAINT asaas_payments_unique_per_env UNIQUE (asaas_payment_id, environment)
);
COMMENT ON TABLE public.asaas_payments IS
'Cobranças geradas no Asaas. Status raw do Asaas; mapeamento pra financial_records.status acontece no JS.';
CREATE INDEX IF NOT EXISTS idx_asaas_payments_record
ON public.asaas_payments (financial_record_id);
CREATE INDEX IF NOT EXISTS idx_asaas_payments_lookup
ON public.asaas_payments (tenant_id, status, due_date)
WHERE cancelled_at IS NULL;
-- ──────────────────────────────────────────────────────────────────────────
-- 3. asaas_webhook_events — idempotência + audit
-- ──────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS public.asaas_webhook_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
event_id text,
event_type text NOT NULL,
asaas_payment_id text,
payload jsonb NOT NULL,
processed_at timestamptz,
processing_error text,
received_at timestamptz DEFAULT now() NOT NULL
);
COMMENT ON TABLE public.asaas_webhook_events IS
'Audit + idempotência de webhooks Asaas. event_id usado pra dedupe (Asaas faz retry).';
-- event_id UNIQUE quando preenchido (Asaas nem sempre manda)
CREATE UNIQUE INDEX IF NOT EXISTS idx_asaas_webhook_events_event_id
ON public.asaas_webhook_events (event_id)
WHERE event_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_asaas_webhook_events_payment
ON public.asaas_webhook_events (asaas_payment_id);
CREATE INDEX IF NOT EXISTS idx_asaas_webhook_events_unprocessed
ON public.asaas_webhook_events (received_at)
WHERE processed_at IS NULL;
-- ──────────────────────────────────────────────────────────────────────────
-- 4. payment_settings — colunas pra config Asaas por tenant
-- ──────────────────────────────────────────────────────────────────────────
-- API keys plaintext nesta migration. Em produção, mover pra pgsodium/Vault.
ALTER TABLE public.payment_settings
ADD COLUMN IF NOT EXISTS asaas_api_key_sandbox text,
ADD COLUMN IF NOT EXISTS asaas_api_key_prod text,
ADD COLUMN IF NOT EXISTS asaas_environment text DEFAULT 'sandbox'
CHECK (asaas_environment IN ('sandbox', 'prod')),
ADD COLUMN IF NOT EXISTS asaas_webhook_token text,
ADD COLUMN IF NOT EXISTS asaas_enabled boolean DEFAULT false NOT NULL;
COMMENT ON COLUMN public.payment_settings.asaas_api_key_sandbox IS
'API key Asaas SANDBOX. PLAINTEXT por enquanto — migrar pra Vault em prod.';
COMMENT ON COLUMN public.payment_settings.asaas_api_key_prod IS
'API key Asaas PRODUÇÃO. PLAINTEXT por enquanto — migrar pra Vault em prod.';
COMMENT ON COLUMN public.payment_settings.asaas_environment IS
'Qual key usar: sandbox (testes) ou prod (real). Default sandbox por segurança.';
COMMENT ON COLUMN public.payment_settings.asaas_webhook_token IS
'Token customizado pra webhook receiver validar. Setar mesmo valor no dashboard Asaas.';
COMMENT ON COLUMN public.payment_settings.asaas_enabled IS
'Flag que controla se gateway Asaas está habilitado pro tenant. Default false (opt-in).';
COMMIT;