|
|
|
@@ -0,0 +1,412 @@
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- F6.2 Lote D — RPCs user-facing roteadas pro schema do tenant
|
|
|
|
|
--
|
|
|
|
|
-- ⚠️ APLICAR COMO supabase_admin (mix de funções owned postgres/supabase_admin).
|
|
|
|
|
-- docker exec -i -e PGPASSWORD=postgres supabase_db_agenciapsi-primesakai \
|
|
|
|
|
-- psql -U supabase_admin -h 127.0.0.1 -d postgres -v ON_ERROR_STOP=1 \
|
|
|
|
|
-- < database-novo/manual/f6_2d_user_rpcs.supabase_admin.sql
|
|
|
|
|
--
|
|
|
|
|
-- Padrão: valida is_tenant_member(p_tenant_id) + set_config search_path pro
|
|
|
|
|
-- schema do tenant; remove `WHERE tenant_id=` e tenant_id de inserts; unqualify
|
|
|
|
|
-- tabelas tenant; %ROWTYPE→RECORD; RETURNS <tabela_tenant>→jsonb.
|
|
|
|
|
-- Tabelas que FICAM em public (audit_logs global; patient_intake_requests,
|
|
|
|
|
-- document_share_links F1b) seguem com `public.` + filtro tenant_id.
|
|
|
|
|
--
|
|
|
|
|
-- list_my_signatures é CROSS-TENANT (assinante em vários tenants) → Lote F.
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
BEGIN;
|
|
|
|
|
|
|
|
|
|
-- helper: valida acesso e RETORNA o schema do tenant. NÃO seta search_path
|
|
|
|
|
-- (set_config feito dentro de helper com SET search_path próprio seria revertido
|
|
|
|
|
-- na saída do helper). Cada RPC faz: PERFORM set_config('search_path',
|
|
|
|
|
-- public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
|
|
|
|
CREATE OR REPLACE FUNCTION public._tenant_route(p_tenant_id uuid)
|
|
|
|
|
RETURNS text LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
|
|
|
|
AS $$
|
|
|
|
|
DECLARE v_schema text;
|
|
|
|
|
BEGIN
|
|
|
|
|
IF p_tenant_id IS NULL THEN RAISE EXCEPTION 'p_tenant_id obrigatório'; END IF;
|
|
|
|
|
IF NOT public.is_tenant_member(p_tenant_id) AND NOT public.is_saas_admin() THEN
|
|
|
|
|
RAISE EXCEPTION 'Sem permissão no tenant %', p_tenant_id USING ERRCODE='42501';
|
|
|
|
|
END IF;
|
|
|
|
|
v_schema := public.tenant_schema_for(p_tenant_id);
|
|
|
|
|
IF v_schema IS NULL THEN RAISE EXCEPTION 'schema não encontrado p/ tenant %', p_tenant_id; END IF;
|
|
|
|
|
RETURN v_schema;
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
-- ───────────────────────────────────────────────────────────────────────────
|
|
|
|
|
-- GRUPO 1 — já têm p_tenant_id, RETURNS jsonb/void (CREATE OR REPLACE)
|
|
|
|
|
-- ───────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
CREATE OR REPLACE FUNCTION public.delete_commitment_full(p_tenant_id uuid, p_commitment_id uuid)
|
|
|
|
|
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
|
|
|
|
AS $$
|
|
|
|
|
DECLARE v_is_native boolean; v_fields int:=0; v_logs int:=0; v_parent int:=0;
|
|
|
|
|
BEGIN
|
|
|
|
|
PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
|
|
|
|
SELECT dc.is_native INTO v_is_native FROM determined_commitments dc WHERE dc.id = p_commitment_id;
|
|
|
|
|
IF v_is_native IS NULL THEN RAISE EXCEPTION 'Commitment not found'; END IF;
|
|
|
|
|
IF v_is_native = true THEN RAISE EXCEPTION 'Cannot delete native commitment'; END IF;
|
|
|
|
|
DELETE FROM determined_commitment_fields WHERE commitment_id = p_commitment_id; GET DIAGNOSTICS v_fields = ROW_COUNT;
|
|
|
|
|
DELETE FROM commitment_time_logs WHERE commitment_id = p_commitment_id; GET DIAGNOSTICS v_logs = ROW_COUNT;
|
|
|
|
|
DELETE FROM determined_commitments WHERE id = p_commitment_id; GET DIAGNOSTICS v_parent = ROW_COUNT;
|
|
|
|
|
IF v_parent <> 1 THEN RAISE EXCEPTION 'Parent not deleted'; END IF;
|
|
|
|
|
RETURN jsonb_build_object('ok',true,'deleted',jsonb_build_object('fields',v_fields,'logs',v_logs,'commitment',v_parent));
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
CREATE OR REPLACE FUNCTION public.delete_determined_commitment(p_tenant_id uuid, p_commitment_id uuid)
|
|
|
|
|
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
|
|
|
|
AS $$
|
|
|
|
|
DECLARE v_is_native boolean; v_fields int:=0; v_logs int:=0; v_parent int:=0;
|
|
|
|
|
BEGIN
|
|
|
|
|
PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
|
|
|
|
SELECT dc.is_native INTO v_is_native FROM determined_commitments dc WHERE dc.id = p_commitment_id;
|
|
|
|
|
IF v_is_native IS NULL THEN RAISE EXCEPTION 'Commitment not found for tenant'; END IF;
|
|
|
|
|
IF v_is_native = true THEN RAISE EXCEPTION 'Cannot delete native commitment'; END IF;
|
|
|
|
|
DELETE FROM determined_commitment_fields WHERE commitment_id = p_commitment_id; GET DIAGNOSTICS v_fields = ROW_COUNT;
|
|
|
|
|
DELETE FROM commitment_time_logs WHERE commitment_id = p_commitment_id; GET DIAGNOSTICS v_logs = ROW_COUNT;
|
|
|
|
|
DELETE FROM determined_commitments WHERE id = p_commitment_id; GET DIAGNOSTICS v_parent = ROW_COUNT;
|
|
|
|
|
IF v_parent <> 1 THEN RAISE EXCEPTION 'Delete did not remove the commitment'; END IF;
|
|
|
|
|
RETURN jsonb_build_object('ok',true,'tenant_id',p_tenant_id,'commitment_id',p_commitment_id,
|
|
|
|
|
'deleted',jsonb_build_object('fields',v_fields,'logs',v_logs,'commitment',v_parent));
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
CREATE OR REPLACE FUNCTION public.seed_default_patient_groups(p_tenant_id uuid)
|
|
|
|
|
RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
|
|
|
|
AS $$
|
|
|
|
|
DECLARE v_owner_id uuid; v_schema text;
|
|
|
|
|
BEGIN
|
|
|
|
|
v_schema := public.tenant_schema_for(p_tenant_id);
|
|
|
|
|
IF v_schema IS NULL THEN RETURN; END IF; -- schema ainda não existe (chamado antes do clone): no-op
|
|
|
|
|
SELECT user_id INTO v_owner_id FROM public.tenant_members
|
|
|
|
|
WHERE tenant_id = p_tenant_id AND role='tenant_admin' AND status='active' LIMIT 1;
|
|
|
|
|
IF v_owner_id IS NULL THEN RETURN; END IF;
|
|
|
|
|
PERFORM set_config('search_path', v_schema || ',public,pg_temp', true);
|
|
|
|
|
INSERT INTO patient_groups (owner_id, nome, cor, is_system)
|
|
|
|
|
VALUES (v_owner_id,'Crianças','#60a5fa',true),
|
|
|
|
|
(v_owner_id,'Adolescentes','#a78bfa',true),
|
|
|
|
|
(v_owner_id,'Idosos','#34d399',true)
|
|
|
|
|
ON CONFLICT (owner_id, nome) DO NOTHING;
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
-- seed_determined_commitments: idêntico em estrutura, sem tenant_id nos inserts.
|
|
|
|
|
-- Recriado integralmente.
|
|
|
|
|
CREATE OR REPLACE FUNCTION public.seed_determined_commitments(p_tenant_id uuid)
|
|
|
|
|
RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
|
|
|
|
AS $$
|
|
|
|
|
DECLARE v_id uuid; v_schema text;
|
|
|
|
|
BEGIN
|
|
|
|
|
v_schema := public.tenant_schema_for(p_tenant_id);
|
|
|
|
|
IF v_schema IS NULL THEN RETURN; END IF;
|
|
|
|
|
PERFORM set_config('search_path', v_schema || ',public,pg_temp', true);
|
|
|
|
|
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM determined_commitments WHERE is_native=true AND native_key='session') THEN
|
|
|
|
|
INSERT INTO determined_commitments (is_native, native_key, is_locked, active, name, description)
|
|
|
|
|
VALUES (true,'session',true,true,'Sessão','Sessão com paciente');
|
|
|
|
|
END IF;
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM determined_commitments WHERE is_native=true AND native_key='reading') THEN
|
|
|
|
|
INSERT INTO determined_commitments (is_native, native_key, is_locked, active, name, description)
|
|
|
|
|
VALUES (true,'reading',false,true,'Leitura','Praticar leitura');
|
|
|
|
|
END IF;
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM determined_commitments WHERE is_native=true AND native_key='supervision') THEN
|
|
|
|
|
INSERT INTO determined_commitments (is_native, native_key, is_locked, active, name, description)
|
|
|
|
|
VALUES (true,'supervision',false,true,'Supervisão','Supervisão');
|
|
|
|
|
END IF;
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM determined_commitments WHERE is_native=true AND native_key='class') THEN
|
|
|
|
|
INSERT INTO determined_commitments (is_native, native_key, is_locked, active, name, description)
|
|
|
|
|
VALUES (true,'class',false,false,'Aula','Dar aula');
|
|
|
|
|
END IF;
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM determined_commitments WHERE is_native=true AND native_key='analysis') THEN
|
|
|
|
|
INSERT INTO determined_commitments (is_native, native_key, is_locked, active, name, description)
|
|
|
|
|
VALUES (true,'analysis',false,true,'Análise Pessoal','Minha análise pessoal');
|
|
|
|
|
END IF;
|
|
|
|
|
|
|
|
|
|
SELECT id INTO v_id FROM determined_commitments WHERE is_native=true AND native_key='session' LIMIT 1;
|
|
|
|
|
IF v_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM determined_commitment_fields WHERE commitment_id=v_id AND key='notes') THEN
|
|
|
|
|
INSERT INTO determined_commitment_fields (commitment_id, key, label, field_type, required, sort_order)
|
|
|
|
|
VALUES (v_id,'notes','Observação','textarea',false,30);
|
|
|
|
|
END IF;
|
|
|
|
|
SELECT id INTO v_id FROM determined_commitments WHERE is_native=true AND native_key='reading' LIMIT 1;
|
|
|
|
|
IF v_id IS NOT NULL THEN
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM determined_commitment_fields WHERE commitment_id=v_id AND key='book') THEN
|
|
|
|
|
INSERT INTO determined_commitment_fields (commitment_id, key, label, field_type, required, sort_order) VALUES (v_id,'book','Livro','text',false,10);
|
|
|
|
|
END IF;
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM determined_commitment_fields WHERE commitment_id=v_id AND key='author') THEN
|
|
|
|
|
INSERT INTO determined_commitment_fields (commitment_id, key, label, field_type, required, sort_order) VALUES (v_id,'author','Autor','text',false,20);
|
|
|
|
|
END IF;
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM determined_commitment_fields WHERE commitment_id=v_id AND key='notes') THEN
|
|
|
|
|
INSERT INTO determined_commitment_fields (commitment_id, key, label, field_type, required, sort_order) VALUES (v_id,'notes','Observação','textarea',false,30);
|
|
|
|
|
END IF;
|
|
|
|
|
END IF;
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
-- ───────────────────────────────────────────────────────────────────────────
|
|
|
|
|
-- GRUPO 2 — novo p_tenant_id (1º param), RETURNS scalar/jsonb (DROP+CREATE)
|
|
|
|
|
-- ───────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
DROP FUNCTION IF EXISTS public.cancel_recurrence_from(uuid, date);
|
|
|
|
|
CREATE FUNCTION public.cancel_recurrence_from(p_tenant_id uuid, p_recurrence_id uuid, p_from_date date)
|
|
|
|
|
RETURNS void LANGUAGE plpgsql 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);
|
|
|
|
|
UPDATE recurrence_rules
|
|
|
|
|
SET end_date = p_from_date - INTERVAL '1 day', open_ended = false,
|
|
|
|
|
status = CASE WHEN p_from_date <= start_date THEN 'cancelado' ELSE status END,
|
|
|
|
|
updated_at = now()
|
|
|
|
|
WHERE id = p_recurrence_id;
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
DROP FUNCTION IF EXISTS public.cancelar_eventos_serie(uuid, timestamptz);
|
|
|
|
|
CREATE FUNCTION public.cancelar_eventos_serie(p_tenant_id uuid, p_serie_id uuid, p_a_partir_de timestamptz DEFAULT now())
|
|
|
|
|
RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
|
|
|
|
AS $$
|
|
|
|
|
DECLARE v_count integer;
|
|
|
|
|
BEGIN
|
|
|
|
|
PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
|
|
|
|
UPDATE agenda_eventos SET status='cancelado', updated_at=now()
|
|
|
|
|
WHERE serie_id = p_serie_id AND inicio_em >= p_a_partir_de AND status NOT IN ('realizado','cancelado');
|
|
|
|
|
GET DIAGNOSTICS v_count = ROW_COUNT;
|
|
|
|
|
RETURN v_count;
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
DROP FUNCTION IF EXISTS public.split_recurrence_at(uuid, date);
|
|
|
|
|
CREATE FUNCTION public.split_recurrence_at(p_tenant_id uuid, p_recurrence_id uuid, p_from_date date)
|
|
|
|
|
RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
|
|
|
|
AS $$
|
|
|
|
|
DECLARE v_old RECORD; v_new_id uuid;
|
|
|
|
|
BEGIN
|
|
|
|
|
PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
|
|
|
|
SELECT * INTO v_old FROM recurrence_rules WHERE id = p_recurrence_id;
|
|
|
|
|
IF NOT FOUND THEN RAISE EXCEPTION 'recurrence_rule % não encontrada', p_recurrence_id; END IF;
|
|
|
|
|
UPDATE recurrence_rules SET end_date = p_from_date - INTERVAL '1 day', open_ended=false, updated_at=now()
|
|
|
|
|
WHERE id = p_recurrence_id;
|
|
|
|
|
INSERT INTO recurrence_rules (owner_id, therapist_id, patient_id, determined_commitment_id, type, interval, weekdays,
|
|
|
|
|
start_time, end_time, timezone, duration_min, start_date, end_date, max_occurrences, open_ended,
|
|
|
|
|
modalidade, titulo_custom, observacoes, extra_fields, status)
|
|
|
|
|
SELECT owner_id, therapist_id, patient_id, determined_commitment_id, type, interval, weekdays,
|
|
|
|
|
start_time, end_time, timezone, duration_min, p_from_date, v_old.end_date, v_old.max_occurrences, v_old.open_ended,
|
|
|
|
|
modalidade, titulo_custom, observacoes, extra_fields, status
|
|
|
|
|
FROM recurrence_rules WHERE id = p_recurrence_id
|
|
|
|
|
RETURNING id INTO v_new_id;
|
|
|
|
|
RETURN v_new_id;
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
-- can_delete_patient: SQL sem SET search_path → herda o do chamador (schema).
|
|
|
|
|
-- Unqualified pra resolver no schema do tenant ativo.
|
|
|
|
|
CREATE OR REPLACE FUNCTION public.can_delete_patient(p_patient_id uuid)
|
|
|
|
|
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER
|
|
|
|
|
AS $$
|
|
|
|
|
SELECT NOT EXISTS (
|
|
|
|
|
SELECT 1 FROM agenda_eventos WHERE patient_id = p_patient_id
|
|
|
|
|
UNION ALL
|
|
|
|
|
SELECT 1 FROM recurrence_rules WHERE patient_id = p_patient_id
|
|
|
|
|
UNION ALL
|
|
|
|
|
SELECT 1 FROM billing_contracts WHERE patient_id = p_patient_id
|
|
|
|
|
);
|
|
|
|
|
$$;
|
|
|
|
|
|
|
|
|
|
DROP FUNCTION IF EXISTS public.safe_delete_patient(uuid);
|
|
|
|
|
CREATE FUNCTION public.safe_delete_patient(p_tenant_id uuid, p_patient_id uuid)
|
|
|
|
|
RETURNS jsonb LANGUAGE plpgsql 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);
|
|
|
|
|
IF NOT public.can_delete_patient(p_patient_id) THEN
|
|
|
|
|
RETURN jsonb_build_object('ok',false,'error','has_history',
|
|
|
|
|
'message','Este paciente possui histórico clínico ou financeiro e não pode ser removido. Você pode desativar ou arquivar o paciente.');
|
|
|
|
|
END IF;
|
|
|
|
|
-- ownership: owner_id direto ou responsible_member do caller (tenant_members é GLOBAL)
|
|
|
|
|
IF NOT EXISTS (SELECT 1 FROM patients
|
|
|
|
|
WHERE id = p_patient_id AND (owner_id = auth.uid()
|
|
|
|
|
OR responsible_member_id IN (SELECT id FROM public.tenant_members WHERE user_id = auth.uid()))) THEN
|
|
|
|
|
RETURN jsonb_build_object('ok',false,'error','forbidden','message','Sem permissão para excluir este paciente.');
|
|
|
|
|
END IF;
|
|
|
|
|
DELETE FROM patients WHERE id = p_patient_id;
|
|
|
|
|
RETURN jsonb_build_object('ok',true);
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
DROP FUNCTION IF EXISTS public.export_patient_data(uuid);
|
|
|
|
|
CREATE FUNCTION public.export_patient_data(p_tenant_id uuid, p_patient_id uuid)
|
|
|
|
|
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
|
|
|
|
AS $$
|
|
|
|
|
DECLARE v_patient RECORD; v_caller uuid; v_result jsonb;
|
|
|
|
|
BEGIN
|
|
|
|
|
v_caller := auth.uid();
|
|
|
|
|
IF v_caller IS NULL THEN RAISE EXCEPTION 'Autenticacao obrigatoria' USING ERRCODE='28000'; END IF;
|
|
|
|
|
PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
|
|
|
|
SELECT * INTO v_patient FROM patients WHERE id = p_patient_id;
|
|
|
|
|
IF NOT FOUND THEN RAISE EXCEPTION 'Paciente nao encontrado' USING ERRCODE='P0002'; END IF;
|
|
|
|
|
v_result := jsonb_build_object(
|
|
|
|
|
'export_metadata', jsonb_build_object('generated_at', now(), 'generated_by', v_caller,
|
|
|
|
|
'tenant_id', p_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 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 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 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 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 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,'status',ae.status,'inicio_em',ae.inicio_em,'fim_em',ae.fim_em) ORDER BY ae.inicio_em) FROM agenda_eventos ae WHERE ae.patient_id = p_patient_id),'[]'::jsonb),
|
|
|
|
|
'documentos', COALESCE((SELECT jsonb_agg(jsonb_build_object('id',d.id,'nome',d.nome_original,'tipo',d.tipo_documento,'criado_em',d.created_at) ORDER BY d.created_at) FROM documents d WHERE d.patient_id = p_patient_id AND d.deleted_at IS NULL),'[]'::jsonb),
|
|
|
|
|
'financeiro', COALESCE((SELECT jsonb_agg(jsonb_build_object('id',fr.id,'tipo',fr.type,'valor',fr.final_amount,'status',fr.status,'vencimento',fr.due_date) ORDER BY fr.created_at) FROM financial_records fr WHERE fr.patient_id = p_patient_id AND fr.deleted_at IS NULL),'[]'::jsonb)
|
|
|
|
|
);
|
|
|
|
|
RETURN v_result;
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
DROP FUNCTION IF EXISTS public.search_global(text, text[], integer);
|
|
|
|
|
CREATE FUNCTION public.search_global(p_tenant_id uuid, p_q text, p_scope text[] DEFAULT NULL, p_limit integer DEFAULT 8)
|
|
|
|
|
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
|
|
|
|
AS $$
|
|
|
|
|
DECLARE
|
|
|
|
|
v_q text; v_pattern text; v_limit int;
|
|
|
|
|
v_patients jsonb:='[]'::jsonb; v_appointments jsonb:='[]'::jsonb; v_documents jsonb:='[]'::jsonb;
|
|
|
|
|
v_services jsonb:='[]'::jsonb; v_intakes jsonb:='[]'::jsonb;
|
|
|
|
|
BEGIN
|
|
|
|
|
PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
|
|
|
|
v_q := nullif(btrim(coalesce(p_q,'')),'');
|
|
|
|
|
IF v_q IS NULL OR length(v_q) < 2 THEN
|
|
|
|
|
RETURN jsonb_build_object('patients','[]'::jsonb,'appointments','[]'::jsonb,'documents','[]'::jsonb,'services','[]'::jsonb,'intakes','[]'::jsonb);
|
|
|
|
|
END IF;
|
|
|
|
|
v_q := left(v_q,80); v_pattern := '%'||v_q||'%'; v_limit := GREATEST(1, LEAST(coalesce(p_limit,8),20));
|
|
|
|
|
|
|
|
|
|
IF p_scope IS NULL OR 'patients' = ANY(p_scope) THEN
|
|
|
|
|
SELECT coalesce(jsonb_agg(jsonb_build_object('id',id,'label',nome_completo,
|
|
|
|
|
'sublabel',coalesce(nullif(email_principal,''),nullif(telefone,''),''),'avatar_url',avatar_url,
|
|
|
|
|
'deeplink','/therapist/patients/cadastro/'||id::text,'score',round(score::numeric,3))),'[]'::jsonb) INTO v_patients
|
|
|
|
|
FROM (SELECT p.id,p.nome_completo,p.email_principal,p.telefone,p.avatar_url,
|
|
|
|
|
GREATEST(similarity(coalesce(p.nome_completo,''),v_q),similarity(coalesce(p.email_principal,''),v_q)*0.7,
|
|
|
|
|
similarity(coalesce(p.telefone,''),v_q)*0.5,similarity(coalesce(p.cpf,''),v_q)*0.6) AS score
|
|
|
|
|
FROM patients p WHERE p.nome_completo ILIKE v_pattern OR p.email_principal ILIKE v_pattern OR p.telefone ILIKE v_pattern OR p.cpf ILIKE v_pattern
|
|
|
|
|
ORDER BY score DESC, p.nome_completo ASC LIMIT v_limit) ranked;
|
|
|
|
|
END IF;
|
|
|
|
|
IF p_scope IS NULL OR 'appointments' = ANY(p_scope) THEN
|
|
|
|
|
SELECT coalesce(jsonb_agg(jsonb_build_object('id',id,'label',label,
|
|
|
|
|
'sublabel',trim(both ' · ' from coalesce(patient_name,')')||' · '||to_char(inicio_em,'DD/MM/YYYY HH24:MI')),
|
|
|
|
|
'deeplink','/therapist/agenda?event='||id::text,'score',round(score::numeric,3))),'[]'::jsonb) INTO v_appointments
|
|
|
|
|
FROM (SELECT e.id, coalesce(nullif(e.titulo_custom,''),nullif(e.titulo,''),'Sessão') AS label, e.inicio_em, pat.nome_completo AS patient_name,
|
|
|
|
|
GREATEST(similarity(coalesce(e.titulo,''),v_q),similarity(coalesce(e.titulo_custom,''),v_q),similarity(coalesce(pat.nome_completo,''),v_q)*0.9) AS score
|
|
|
|
|
FROM agenda_eventos e LEFT JOIN patients pat ON pat.id = e.patient_id
|
|
|
|
|
WHERE e.titulo ILIKE v_pattern OR e.titulo_custom ILIKE v_pattern OR pat.nome_completo ILIKE v_pattern
|
|
|
|
|
ORDER BY score DESC, e.inicio_em DESC LIMIT v_limit) ranked;
|
|
|
|
|
END IF;
|
|
|
|
|
IF p_scope IS NULL OR 'documents' = ANY(p_scope) THEN
|
|
|
|
|
SELECT coalesce(jsonb_agg(jsonb_build_object('id',id,'label',nome_original,
|
|
|
|
|
'sublabel',trim(both ' · ' from coalesce(patient_name,'')||' · '||coalesce(tipo_documento,'')),
|
|
|
|
|
'deeplink','/therapist/patients/'||patient_id::text||'/documents','score',round(score::numeric,3))),'[]'::jsonb) INTO v_documents
|
|
|
|
|
FROM (SELECT d.id,d.patient_id,d.nome_original,d.tipo_documento,pat.nome_completo AS patient_name,
|
|
|
|
|
GREATEST(similarity(coalesce(d.nome_original,''),v_q),similarity(coalesce(d.descricao,''),v_q)*0.7) AS score
|
|
|
|
|
FROM documents d LEFT JOIN patients pat ON pat.id = d.patient_id
|
|
|
|
|
WHERE d.nome_original ILIKE v_pattern OR d.descricao ILIKE v_pattern
|
|
|
|
|
ORDER BY score DESC, d.nome_original ASC LIMIT v_limit) ranked;
|
|
|
|
|
END IF;
|
|
|
|
|
IF p_scope IS NULL OR 'services' = ANY(p_scope) THEN
|
|
|
|
|
SELECT coalesce(jsonb_agg(jsonb_build_object('id',id,'label',name,
|
|
|
|
|
'sublabel',trim(both ' · ' from 'R$ '||to_char(price,'FM999G999G990D00')||' · '||coalesce(duration_min::text||' min','')),
|
|
|
|
|
'deeplink','/configuracoes/precificacao','score',round(score::numeric,3))),'[]'::jsonb) INTO v_services
|
|
|
|
|
FROM (SELECT s.id,s.name,s.price,s.duration_min,
|
|
|
|
|
GREATEST(similarity(coalesce(s.name,''),v_q),similarity(coalesce(s.description,''),v_q)*0.7) AS score
|
|
|
|
|
FROM services s WHERE s.active IS TRUE AND (s.name ILIKE v_pattern OR s.description ILIKE v_pattern)
|
|
|
|
|
ORDER BY score DESC, s.name ASC LIMIT v_limit) ranked;
|
|
|
|
|
END IF;
|
|
|
|
|
-- intakes: patient_intake_requests FICA em public (F1b) → qualifica + filtra tenant_id
|
|
|
|
|
IF p_scope IS NULL OR 'intakes' = ANY(p_scope) THEN
|
|
|
|
|
SELECT coalesce(jsonb_agg(jsonb_build_object('id',id,
|
|
|
|
|
'label',coalesce(nullif(trim(nome_completo),''),'(sem nome)'),
|
|
|
|
|
'sublabel',trim(both ' · ' from coalesce(nullif(email_principal,''),nullif(telefone,''),'')||' · '||'recebido '||to_char(created_at,'DD/MM/YYYY')),
|
|
|
|
|
'deeplink','/therapist/patients/cadastro/recebidos?id='||id::text,'score',round(score::numeric,3))),'[]'::jsonb) INTO v_intakes
|
|
|
|
|
FROM (SELECT r.id,r.nome_completo,r.email_principal,r.telefone,r.created_at,
|
|
|
|
|
GREATEST(similarity(coalesce(r.nome_completo,''),v_q),similarity(coalesce(r.email_principal,''),v_q)*0.7,similarity(coalesce(r.telefone,''),v_q)*0.5) AS score
|
|
|
|
|
FROM public.patient_intake_requests r
|
|
|
|
|
WHERE r.tenant_id = p_tenant_id AND r.status='new'
|
|
|
|
|
AND (r.nome_completo ILIKE v_pattern OR r.email_principal ILIKE v_pattern OR r.telefone ILIKE v_pattern)
|
|
|
|
|
ORDER BY score DESC, r.created_at DESC LIMIT v_limit) ranked;
|
|
|
|
|
END IF;
|
|
|
|
|
|
|
|
|
|
RETURN jsonb_build_object('patients',v_patients,'appointments',v_appointments,'documents',v_documents,'services',v_services,'intakes',v_intakes);
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
-- ───────────────────────────────────────────────────────────────────────────
|
|
|
|
|
-- GRUPO 3 — RETURNS <tabela_tenant> → jsonb (ripple no FE)
|
|
|
|
|
-- ───────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
DROP FUNCTION IF EXISTS public.mark_as_paid(uuid, text);
|
|
|
|
|
CREATE FUNCTION public.mark_as_paid(p_tenant_id uuid, p_financial_record_id uuid, p_payment_method text)
|
|
|
|
|
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
|
|
|
|
AS $$
|
|
|
|
|
DECLARE v_record RECORD;
|
|
|
|
|
BEGIN
|
|
|
|
|
PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
|
|
|
|
SELECT * INTO v_record FROM financial_records WHERE id = p_financial_record_id AND owner_id = auth.uid() AND deleted_at IS NULL;
|
|
|
|
|
IF NOT FOUND THEN RAISE EXCEPTION 'Registro financeiro não encontrado ou sem permissão.'; END IF;
|
|
|
|
|
IF v_record.status NOT IN ('pending','overdue') THEN RAISE EXCEPTION 'Apenas cobranças pendentes ou vencidas podem ser marcadas como pagas.'; END IF;
|
|
|
|
|
UPDATE financial_records SET status='paid', paid_at=now(), payment_method=p_payment_method, updated_at=now()
|
|
|
|
|
WHERE id = p_financial_record_id RETURNING * INTO v_record;
|
|
|
|
|
RETURN to_jsonb(v_record);
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
DROP FUNCTION IF EXISTS public.create_financial_record_for_session(uuid, uuid, uuid, uuid, numeric, date);
|
|
|
|
|
CREATE FUNCTION public.create_financial_record_for_session(p_tenant_id uuid, p_owner_id uuid, p_patient_id uuid, p_agenda_evento_id uuid, p_amount numeric, p_due_date date)
|
|
|
|
|
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
|
|
|
|
AS $$
|
|
|
|
|
DECLARE v_existing RECORD; v_new RECORD;
|
|
|
|
|
BEGIN
|
|
|
|
|
PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
|
|
|
|
SELECT * INTO v_existing FROM financial_records WHERE agenda_evento_id = p_agenda_evento_id AND deleted_at IS NULL AND status != 'cancelled' LIMIT 1;
|
|
|
|
|
IF FOUND THEN RETURN to_jsonb(v_existing); END IF;
|
|
|
|
|
INSERT INTO financial_records (owner_id, patient_id, agenda_evento_id, amount, discount_amount, final_amount, status, due_date)
|
|
|
|
|
VALUES (p_owner_id, p_patient_id, p_agenda_evento_id, p_amount, 0, p_amount, 'pending', p_due_date)
|
|
|
|
|
RETURNING * INTO v_new;
|
|
|
|
|
UPDATE agenda_eventos SET billed = TRUE WHERE id = p_agenda_evento_id;
|
|
|
|
|
RETURN to_jsonb(v_new);
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
DROP FUNCTION IF EXISTS public.mark_payout_as_paid(uuid);
|
|
|
|
|
CREATE FUNCTION public.mark_payout_as_paid(p_tenant_id uuid, p_payout_id uuid)
|
|
|
|
|
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
|
|
|
|
AS $$
|
|
|
|
|
DECLARE v_payout RECORD;
|
|
|
|
|
BEGIN
|
|
|
|
|
PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
|
|
|
|
SELECT * INTO v_payout FROM therapist_payouts WHERE id = p_payout_id;
|
|
|
|
|
IF NOT FOUND THEN RAISE EXCEPTION 'Repasse não encontrado: %', p_payout_id; END IF;
|
|
|
|
|
IF NOT public.is_tenant_admin(p_tenant_id) THEN RAISE EXCEPTION 'Apenas o administrador da clínica pode marcar repasses como pagos.'; END IF;
|
|
|
|
|
IF v_payout.status <> 'pending' THEN RAISE EXCEPTION 'Repasse já está com status ''%''. Apenas repasses pendentes podem ser pagos.', v_payout.status; END IF;
|
|
|
|
|
UPDATE therapist_payouts SET status='paid', paid_at=now(), updated_at=now() WHERE id = p_payout_id RETURNING * INTO v_payout;
|
|
|
|
|
RETURN to_jsonb(v_payout);
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
DROP FUNCTION IF EXISTS public.create_therapist_payout(uuid, uuid, date, date);
|
|
|
|
|
CREATE FUNCTION public.create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date)
|
|
|
|
|
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
|
|
|
|
AS $$
|
|
|
|
|
DECLARE v_payout RECORD; v_total int; v_gross numeric(10,2); v_clinic_fee numeric(10,2); v_net numeric(10,2);
|
|
|
|
|
BEGIN
|
|
|
|
|
IF auth.uid() <> p_therapist_id AND NOT public.is_tenant_admin(p_tenant_id) THEN
|
|
|
|
|
RAISE EXCEPTION 'Sem permissão para criar repasse para este terapeuta.';
|
|
|
|
|
END IF;
|
|
|
|
|
PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
|
|
|
|
|
IF EXISTS (SELECT 1 FROM therapist_payouts WHERE owner_id=p_therapist_id AND period_start=p_period_start AND period_end=p_period_end AND status<>'cancelled') THEN
|
|
|
|
|
RAISE EXCEPTION 'Já existe um repasse ativo para o período % a % deste terapeuta.', p_period_start, p_period_end;
|
|
|
|
|
END IF;
|
|
|
|
|
SELECT COUNT(*), COALESCE(SUM(amount),0), COALESCE(SUM(clinic_fee_amount),0), COALESCE(SUM(net_amount),0)
|
|
|
|
|
INTO v_total, v_gross, v_clinic_fee, v_net
|
|
|
|
|
FROM financial_records fr
|
|
|
|
|
WHERE fr.owner_id=p_therapist_id AND fr.type='receita' AND fr.status='paid' AND fr.deleted_at IS NULL
|
|
|
|
|
AND fr.paid_at::date BETWEEN p_period_start AND p_period_end
|
|
|
|
|
AND NOT EXISTS (SELECT 1 FROM therapist_payout_records tpr WHERE tpr.financial_record_id = fr.id);
|
|
|
|
|
IF v_total = 0 THEN RAISE EXCEPTION 'Nenhum registro financeiro elegível encontrado para o período % a %.', p_period_start, p_period_end; END IF;
|
|
|
|
|
INSERT INTO therapist_payouts (owner_id, period_start, period_end, total_sessions, gross_amount, clinic_fee_total, net_amount, status)
|
|
|
|
|
VALUES (p_therapist_id, p_period_start, p_period_end, v_total, v_gross, v_clinic_fee, v_net, 'pending')
|
|
|
|
|
RETURNING * INTO v_payout;
|
|
|
|
|
INSERT INTO therapist_payout_records (payout_id, financial_record_id)
|
|
|
|
|
SELECT v_payout.id, fr.id FROM financial_records fr
|
|
|
|
|
WHERE fr.owner_id=p_therapist_id AND fr.type='receita' AND fr.status='paid' AND fr.deleted_at IS NULL
|
|
|
|
|
AND fr.paid_at::date BETWEEN p_period_start AND p_period_end
|
|
|
|
|
AND NOT EXISTS (SELECT 1 FROM therapist_payout_records tpr WHERE tpr.financial_record_id = fr.id);
|
|
|
|
|
RETURN to_jsonb(v_payout);
|
|
|
|
|
END $$;
|
|
|
|
|
|
|
|
|
|
COMMIT;
|