-- ============================================================================= -- F6.2 Lote G — funções SQL puras → plpgsql + roteamento por tenant -- -- ⚠️ APLICAR COMO supabase_admin. -- -- SQL puro não permite set_config dinâmico do search_path (limitação 3 do -- blueprint) → converter pra plpgsql. Adicionam p_tenant_id + _tenant_route. -- RETURNS SETOF → jsonb. get_entity_primary_phone (interno, -- 0 callers) herda search_path do chamador (sem SET, unqualified). -- ============================================================================= BEGIN; DROP FUNCTION IF EXISTS public.get_financial_summary(uuid, integer, integer); DROP FUNCTION IF EXISTS public.get_financial_summary(uuid, uuid, integer, integer); CREATE FUNCTION public.get_financial_summary(p_tenant_id uuid, p_owner_id uuid, p_year integer, p_month integer) RETURNS TABLE(total_receitas numeric, total_despesas numeric, total_pendente numeric, saldo_liquido numeric, total_repasse numeric, count_receitas bigint, count_despesas bigint) LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ BEGIN PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true); RETURN QUERY SELECT COALESCE(SUM(amount) FILTER (WHERE type='receita' AND status='paid'), 0), COALESCE(SUM(amount) FILTER (WHERE type='despesa' AND status='paid'), 0), COALESCE(SUM(amount) FILTER (WHERE status IN ('pending','overdue')), 0), COALESCE(SUM(amount) FILTER (WHERE type='receita' AND status='paid'), 0) - COALESCE(SUM(amount) FILTER (WHERE type='despesa' AND status='paid'), 0), COALESCE(SUM(clinic_fee_amount) FILTER (WHERE type='receita' AND status='paid'), 0), COUNT(*) FILTER (WHERE type='receita' AND deleted_at IS NULL), COUNT(*) FILTER (WHERE type='despesa' AND deleted_at IS NULL) FROM financial_records WHERE owner_id = p_owner_id AND deleted_at IS NULL AND EXTRACT(YEAR FROM COALESCE(paid_at::date, due_date, created_at::date)) = p_year AND EXTRACT(MONTH FROM COALESCE(paid_at::date, due_date, created_at::date)) = p_month; END $$; -- list_financial_records: RETURNS SETOF financial_records → jsonb (array) DROP FUNCTION IF EXISTS public.list_financial_records(uuid, integer, integer, text, text, uuid, integer, integer); DROP FUNCTION IF EXISTS public.list_financial_records(uuid, uuid, integer, integer, text, text, uuid, integer, integer); CREATE FUNCTION public.list_financial_records(p_tenant_id uuid, p_owner_id uuid, p_year integer DEFAULT NULL, p_month integer DEFAULT NULL, p_type text DEFAULT NULL, p_status text DEFAULT NULL, p_patient_id uuid DEFAULT NULL, p_limit integer DEFAULT 50, p_offset integer DEFAULT 0) RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ DECLARE v_result jsonb; BEGIN PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true); SELECT COALESCE(jsonb_agg(row_json), '[]'::jsonb) INTO v_result FROM ( SELECT to_jsonb(fr) AS row_json FROM financial_records fr WHERE fr.owner_id = p_owner_id AND fr.deleted_at IS NULL AND (p_type IS NULL OR fr.type::text = p_type) AND (p_status IS NULL OR fr.status = p_status) AND (p_patient_id IS NULL OR fr.patient_id = p_patient_id) AND (p_year IS NULL OR EXTRACT(YEAR FROM COALESCE(fr.paid_at::date, fr.due_date, fr.created_at::date)) = p_year) AND (p_month IS NULL OR EXTRACT(MONTH FROM COALESCE(fr.paid_at::date, fr.due_date, fr.created_at::date)) = p_month) ORDER BY COALESCE(fr.paid_at, fr.due_date::timestamptz, fr.created_at) DESC LIMIT p_limit OFFSET p_offset ) sub; RETURN v_result; END $$; DROP FUNCTION IF EXISTS public.get_patient_session_counts(uuid[]); DROP FUNCTION IF EXISTS public.get_patient_session_counts(uuid, uuid[]); CREATE FUNCTION public.get_patient_session_counts(p_tenant_id uuid, p_patient_ids uuid[]) RETURNS TABLE(patient_id uuid, session_count integer, last_session_at timestamptz) LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ BEGIN PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true); RETURN QUERY SELECT ae.patient_id, COUNT(*)::int, MAX(ae.inicio_em) FROM agenda_eventos ae WHERE ae.patient_id = ANY(p_patient_ids) GROUP BY ae.patient_id; END $$; DROP FUNCTION IF EXISTS public.get_financial_report(uuid, date, date, text); DROP FUNCTION IF EXISTS public.get_financial_report(uuid, uuid, date, date, text); CREATE FUNCTION public.get_financial_report(p_tenant_id uuid, p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text DEFAULT 'month') RETURNS TABLE(group_key text, group_label text, total_receitas numeric, total_despesas numeric, saldo numeric, total_pendente numeric, total_overdue numeric, count_records bigint) LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ BEGIN PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true); RETURN QUERY WITH base AS ( SELECT fr.type, fr.amount, fr.final_amount, fr.status, fr.deleted_at, CASE p_group_by WHEN 'month' THEN TO_CHAR(COALESCE(fr.paid_at::date, fr.due_date, fr.created_at::date),'YYYY-MM') WHEN 'week' THEN TO_CHAR(COALESCE(fr.paid_at::date, fr.due_date, fr.created_at::date),'IYYY-"W"IW') WHEN 'category' THEN COALESCE(fr.category_id::text, fr.category, 'sem_categoria') WHEN 'patient' THEN COALESCE(fr.patient_id::text, 'sem_paciente') ELSE NULL END AS gkey, CASE p_group_by WHEN 'month' THEN TO_CHAR(COALESCE(fr.paid_at::date, fr.due_date, fr.created_at::date),'YYYY-MM') WHEN 'week' THEN TO_CHAR(COALESCE(fr.paid_at::date, fr.due_date, fr.created_at::date),'IYYY-"W"IW') WHEN 'category' THEN COALESCE(fc.name, fr.category, 'Sem categoria') WHEN 'patient' THEN COALESCE(p.nome_completo, fr.patient_id::text, 'Sem paciente') ELSE NULL END AS glabel FROM financial_records fr LEFT JOIN financial_categories fc ON fc.id = fr.category_id LEFT JOIN patients p ON p.id = fr.patient_id WHERE fr.owner_id = p_owner_id AND fr.deleted_at IS NULL AND COALESCE(fr.paid_at::date, fr.due_date, fr.created_at::date) BETWEEN p_start_date AND p_end_date ) SELECT gkey, glabel, COALESCE(SUM(final_amount) FILTER (WHERE type='receita' AND status='paid'),0), COALESCE(SUM(final_amount) FILTER (WHERE type='despesa' AND status='paid'),0), COALESCE(SUM(final_amount) FILTER (WHERE type='receita' AND status='paid'),0) - COALESCE(SUM(final_amount) FILTER (WHERE type='despesa' AND status='paid'),0), COALESCE(SUM(final_amount) FILTER (WHERE status='pending'),0), COALESCE(SUM(final_amount) FILTER (WHERE status='overdue'),0), COUNT(*) FROM base WHERE gkey IS NOT NULL GROUP BY gkey, glabel ORDER BY gkey ASC; END $$; -- get_entity_primary_phone: interno (0 callers). Sem SET search_path → herda do -- chamador (que roteia pro schema). Unqualified. Mantém assinatura. CREATE OR REPLACE FUNCTION public.get_entity_primary_phone(p_entity_type text, p_entity_id uuid) RETURNS text LANGUAGE sql STABLE SECURITY DEFINER AS $$ SELECT number FROM contact_phones WHERE entity_type = p_entity_type AND entity_id = p_entity_id ORDER BY is_primary DESC, position ASC, created_at ASC LIMIT 1; $$; COMMIT;