-- ============================================================================ -- 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;