-- ============================================================================= -- Migration: 20260419000014_financial_security_hardening -- Sessão 6 — revisão Financeiro. Resolve V#1-V#5 (2 críticos + 3 altos). -- V#6-V#11 adiados (médios/baixos com plano). -- -- Auditoria prévia confirmou: -- • 0 financial_records com tenant_id NULL -- • 0 records com clinic_fee_amount > amount -- → seguro aplicar NOT NULL e CHECK constraints. -- ============================================================================= -- ───────────────────────────────────────────────────────────────────────── -- V#1: billing_contracts policy granular -- ----------------------------------------------------------------------------- DROP POLICY IF EXISTS "billing_contracts: owner full access" ON public.billing_contracts; DROP POLICY IF EXISTS "billing_contracts: select" ON public.billing_contracts; DROP POLICY IF EXISTS "billing_contracts: insert" ON public.billing_contracts; DROP POLICY IF EXISTS "billing_contracts: update" ON public.billing_contracts; DROP POLICY IF EXISTS "billing_contracts: delete" ON public.billing_contracts; CREATE POLICY "billing_contracts: select" ON public.billing_contracts FOR SELECT TO authenticated USING ( owner_id = auth.uid() OR public.is_saas_admin() OR tenant_id IN ( SELECT tm.tenant_id FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.status = 'active' ) ); CREATE POLICY "billing_contracts: insert" ON public.billing_contracts FOR INSERT TO authenticated WITH CHECK ( owner_id = auth.uid() AND tenant_id IN ( SELECT tm.tenant_id FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.status = 'active' ) ); CREATE POLICY "billing_contracts: update" ON public.billing_contracts FOR UPDATE TO authenticated USING (owner_id = auth.uid() OR public.is_saas_admin()) WITH CHECK (owner_id = auth.uid() OR public.is_saas_admin()); CREATE POLICY "billing_contracts: delete" ON public.billing_contracts FOR DELETE TO authenticated USING (owner_id = auth.uid() OR public.is_saas_admin()); -- ───────────────────────────────────────────────────────────────────────── -- V#2: financial_records.tenant_id NOT NULL + trigger backfill -- (auditoria: 0 órfãos, seguro aplicar) -- ----------------------------------------------------------------------------- ALTER TABLE public.financial_records ALTER COLUMN tenant_id SET NOT NULL; -- Trigger defensivo: se tentar inserir sem tenant_id, busca via owner_id->tenant_members CREATE OR REPLACE FUNCTION public.financial_records_inject_tenant() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.tenant_id IS NULL AND NEW.owner_id IS NOT NULL THEN SELECT tm.tenant_id INTO NEW.tenant_id FROM public.tenant_members tm WHERE tm.user_id = NEW.owner_id AND tm.status = 'active' ORDER BY tm.created_at DESC LIMIT 1; END IF; RETURN NEW; END; $$; DROP TRIGGER IF EXISTS trg_financial_records_inject_tenant ON public.financial_records; CREATE TRIGGER trg_financial_records_inject_tenant BEFORE INSERT ON public.financial_records FOR EACH ROW EXECUTE FUNCTION public.financial_records_inject_tenant(); -- ───────────────────────────────────────────────────────────────────────── -- V#5: financial_records CHECK contra net_amount negativo -- ----------------------------------------------------------------------------- ALTER TABLE public.financial_records DROP CONSTRAINT IF EXISTS financial_records_fee_lte_amount_chk; ALTER TABLE public.financial_records ADD CONSTRAINT financial_records_fee_lte_amount_chk CHECK (clinic_fee_amount IS NULL OR (clinic_fee_amount >= 0 AND clinic_fee_amount <= amount)); -- ───────────────────────────────────────────────────────────────────────── -- V#3: payment_settings — adicionar SELECT pra tenant_admin -- ----------------------------------------------------------------------------- DROP POLICY IF EXISTS "payment_settings: tenant_admin read" ON public.payment_settings; CREATE POLICY "payment_settings: tenant_admin read" ON public.payment_settings FOR SELECT TO authenticated USING ( tenant_id IS NOT NULL AND tenant_id IN ( SELECT tm.tenant_id FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.status = 'active' AND tm.role IN ('tenant_admin','admin','owner') ) ); -- (a policy ALL "owner full access" continua — owner mexe nos próprios) -- ───────────────────────────────────────────────────────────────────────── -- V#4: professional_pricing — adicionar SELECT pra tenant_admin -- ----------------------------------------------------------------------------- DROP POLICY IF EXISTS "professional_pricing: tenant_admin read" ON public.professional_pricing; CREATE POLICY "professional_pricing: tenant_admin read" ON public.professional_pricing FOR SELECT TO authenticated USING ( tenant_id IN ( SELECT tm.tenant_id FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.status = 'active' AND tm.role IN ('tenant_admin','admin','owner') ) );