-- ============================================================================= -- Migration: 20260420000004_lgpd_export_patient_rpc -- Sessao 11 - Fase 2b (Opcao C). -- -- Implementa LGPD Art. 18 - direito de portabilidade do titular. -- RPC export_patient_data(p_patient_id uuid) retorna jsonb com todos os dados -- relacionados ao paciente. Registra o evento em audit_logs para rastreabilidade. -- -- Seguranca: -- - SECURITY DEFINER permite agregar tabelas diversas bypassando RLS -- - Verificacao explicita: caller deve ser tenant_member ativo da clinica do paciente -- - Proibido acesso cross-tenant -- ============================================================================= CREATE OR REPLACE FUNCTION public.export_patient_data(p_patient_id UUID) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$ DECLARE v_patient RECORD; v_tenant_id UUID; v_caller UUID; v_is_member BOOLEAN; v_result JSONB; BEGIN v_caller := auth.uid(); IF v_caller IS NULL THEN RAISE EXCEPTION 'Autenticacao obrigatoria' USING ERRCODE = '28000'; END IF; -- carrega paciente SELECT * INTO v_patient FROM public.patients WHERE id = p_patient_id; IF NOT FOUND THEN RAISE EXCEPTION 'Paciente nao encontrado' USING ERRCODE = 'P0002'; END IF; v_tenant_id := v_patient.tenant_id; -- verifica se caller e membro ativo do tenant do paciente SELECT EXISTS ( SELECT 1 FROM public.tenant_members tm WHERE tm.tenant_id = v_tenant_id AND tm.user_id = v_caller AND tm.status = 'active' ) OR public.is_saas_admin() INTO v_is_member; IF NOT v_is_member THEN RAISE EXCEPTION 'Sem permissao para exportar dados deste paciente' USING ERRCODE = '42501'; END IF; -- monta o payload v_result := jsonb_build_object( 'export_metadata', jsonb_build_object( 'generated_at', now(), 'generated_by', v_caller, 'tenant_id', v_tenant_id, 'patient_id', p_patient_id, 'lgpd_basis', 'Art. 18, II - portabilidade dos dados do titular', 'controller', 'AgenciaPSI - Clinica responsavel', 'format_version', '1.0' ), 'paciente', to_jsonb(v_patient), 'contatos', COALESCE(( SELECT jsonb_agg(to_jsonb(pc) ORDER BY pc.created_at) FROM public.patient_contacts pc WHERE pc.patient_id = p_patient_id ), '[]'::jsonb), 'contatos_apoio', COALESCE(( SELECT jsonb_agg(to_jsonb(psc) ORDER BY psc.created_at) FROM public.patient_support_contacts psc WHERE psc.patient_id = p_patient_id ), '[]'::jsonb), 'historico_status', COALESCE(( SELECT jsonb_agg(to_jsonb(psh) ORDER BY psh.alterado_em) FROM public.patient_status_history psh WHERE psh.patient_id = p_patient_id ), '[]'::jsonb), 'timeline', COALESCE(( SELECT jsonb_agg(to_jsonb(pt) ORDER BY pt.ocorrido_em) FROM public.patient_timeline pt WHERE pt.patient_id = p_patient_id ), '[]'::jsonb), 'descontos', COALESCE(( SELECT jsonb_agg(to_jsonb(pd) ORDER BY pd.created_at) FROM public.patient_discounts pd WHERE pd.patient_id = p_patient_id ), '[]'::jsonb), 'eventos_agenda', COALESCE(( SELECT jsonb_agg( jsonb_build_object( 'id', ae.id, 'tipo', ae.tipo, 'inicio_em', ae.inicio_em, 'fim_em', ae.fim_em, 'status', ae.status, 'observacoes', ae.observacoes, 'created_at', ae.created_at ) ORDER BY ae.inicio_em ) FROM public.agenda_eventos ae WHERE ae.patient_id = p_patient_id ), '[]'::jsonb), 'registros_financeiros', COALESCE(( SELECT jsonb_agg( jsonb_build_object( 'id', fr.id, 'amount', fr.amount, 'discount_amount', fr.discount_amount, 'final_amount', fr.final_amount, 'status', fr.status, 'due_date', fr.due_date, 'paid_at', fr.paid_at, 'payment_method', fr.payment_method, 'notes', fr.notes, 'created_at', fr.created_at ) ORDER BY fr.created_at ) FROM public.financial_records fr WHERE fr.patient_id = p_patient_id ), '[]'::jsonb), 'documentos', COALESCE(( SELECT jsonb_agg( jsonb_build_object( 'id', d.id, 'nome_original', d.nome_original, 'tipo_documento', d.tipo_documento, 'categoria', d.categoria, 'descricao', d.descricao, 'mime_type', d.mime_type, 'tamanho_bytes', d.tamanho_bytes, 'status_revisao', d.status_revisao, 'visibilidade', d.visibilidade, 'uploaded_at', d.uploaded_at, 'created_at', d.created_at ) ORDER BY d.created_at ) FROM public.documents d WHERE d.patient_id = p_patient_id AND d.deleted_at IS NULL ), '[]'::jsonb), 'notificacoes_enviadas', COALESCE(( SELECT jsonb_agg( jsonb_build_object( 'id', nl.id, 'channel', nl.channel, 'template_key', nl.template_key, 'recipient_address', nl.recipient_address, 'status', nl.status, 'sent_at', nl.sent_at, 'delivered_at', nl.delivered_at, 'read_at', nl.read_at, 'failure_reason', nl.failure_reason, 'created_at', nl.created_at ) ORDER BY nl.created_at ) FROM public.notification_logs nl WHERE nl.patient_id = p_patient_id ), '[]'::jsonb), 'audit_trail', COALESCE(( SELECT jsonb_agg( jsonb_build_object( 'id', al.id, 'action', al.action, 'entity_type', al.entity_type, 'changed_fields', al.changed_fields, 'user_id', al.user_id, 'created_at', al.created_at ) ORDER BY al.created_at ) FROM public.audit_logs al WHERE al.tenant_id = v_tenant_id AND al.entity_type = 'patients' AND al.entity_id = p_patient_id::text ), '[]'::jsonb), 'acessos_a_documentos', COALESCE(( SELECT jsonb_agg( jsonb_build_object( 'id', dal.id, 'documento_id', dal.documento_id, 'acao', dal.acao, 'user_id', dal.user_id, 'acessado_em', dal.acessado_em ) ORDER BY dal.acessado_em ) FROM public.document_access_logs dal WHERE dal.documento_id IN ( SELECT id FROM public.documents WHERE patient_id = p_patient_id ) ), '[]'::jsonb), 'grupos', COALESCE(( SELECT jsonb_agg(jsonb_build_object('patient_group_id', pgp.patient_group_id)) FROM public.patient_group_patient pgp WHERE pgp.patient_id = p_patient_id ), '[]'::jsonb), 'tags', COALESCE(( SELECT jsonb_agg(jsonb_build_object('tag_id', ppt.tag_id)) FROM public.patient_patient_tag ppt WHERE ppt.patient_id = p_patient_id ), '[]'::jsonb) ); -- registra o export como evento auditavel INSERT INTO public.audit_logs ( tenant_id, user_id, entity_type, entity_id, action, old_values, new_values, changed_fields, metadata ) VALUES ( v_tenant_id, v_caller, 'patients', p_patient_id::text, 'update', NULL, NULL, ARRAY['__lgpd_export__'], jsonb_build_object( 'action_kind', 'lgpd_export', 'lgpd_basis', 'Art. 18, II', 'patient_name', v_patient.nome_completo ) ); RETURN v_result; END; $$; COMMENT ON FUNCTION public.export_patient_data(UUID) IS 'LGPD Art. 18, II - exporta todos os dados do paciente em jsonb portavel. Registra evento em audit_logs.'; REVOKE ALL ON FUNCTION public.export_patient_data(UUID) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.export_patient_data(UUID) TO authenticated;