Files
agenciapsilmno/database-novo/schema/03_functions/patients.sql

434 lines
12 KiB
PL/PgSQL

-- =============================================================================
-- AgenciaPsi — Functions — Pacientes
-- =============================================================================
-- can_delete_patient, safe_delete_patient, create_patient_intake_request,
-- create_patient_intake_request_v2, rotate_patient_invite_token,
-- patients_validate_member_consistency, prevent_system_group_changes,
-- seed_default_patient_groups
-- =============================================================================
CREATE FUNCTION public.can_delete_patient(p_patient_id uuid) RETURNS boolean
LANGUAGE sql STABLE SECURITY DEFINER
AS $$
SELECT NOT EXISTS (
SELECT 1 FROM public.agenda_eventos WHERE patient_id = p_patient_id
UNION ALL
SELECT 1 FROM public.recurrence_rules WHERE patient_id = p_patient_id
UNION ALL
SELECT 1 FROM public.billing_contracts WHERE patient_id = p_patient_id
);
$$;
ALTER FUNCTION public.can_delete_patient(p_patient_id uuid) OWNER TO supabase_admin;
--
-- Name: cancel_notifications_on_opt_out(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.cancel_notifications_on_opt_out() RETURNS trigger
CREATE FUNCTION public.create_patient_intake_request(p_token text, p_name text, p_email text DEFAULT NULL::text, p_phone text DEFAULT NULL::text, p_notes text DEFAULT NULL::text, p_consent boolean DEFAULT false) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
declare
v_owner uuid;
v_active boolean;
v_expires timestamptz;
v_max_uses int;
v_uses int;
v_id uuid;
begin
select owner_id, active, expires_at, max_uses, uses
into v_owner, v_active, v_expires, v_max_uses, v_uses
from public.patient_invites
where token = p_token
limit 1;
if v_owner is null then
raise exception 'Token inválido';
end if;
if v_active is not true then
raise exception 'Link desativado';
end if;
if v_expires is not null and now() > v_expires then
raise exception 'Link expirado';
end if;
if v_max_uses is not null and v_uses >= v_max_uses then
raise exception 'Limite de uso atingido';
end if;
if p_name is null or length(trim(p_name)) = 0 then
raise exception 'Nome é obrigatório';
end if;
insert into public.patient_intake_requests
(owner_id, token, name, email, phone, notes, consent, status)
values
(v_owner, p_token, trim(p_name),
nullif(lower(trim(p_email)), ''),
nullif(trim(p_phone), ''),
nullif(trim(p_notes), ''),
coalesce(p_consent, false),
'new')
returning id into v_id;
update public.patient_invites
set uses = uses + 1
where token = p_token;
return v_id;
end;
$$;
ALTER FUNCTION public.create_patient_intake_request(p_token text, p_name text, p_email text, p_phone text, p_notes text, p_consent boolean) OWNER TO supabase_admin;
--
-- Name: create_patient_intake_request_v2(text, jsonb); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.create_patient_intake_request_v2(p_token text, p_payload jsonb) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $_$
declare
v_owner_id uuid;
v_intake_id uuid;
v_birth_raw text;
v_birth date;
begin
select owner_id
into v_owner_id
from public.patient_invites
where token = p_token;
if v_owner_id is null then
raise exception 'Token inválido ou expirado';
end if;
v_birth_raw := nullif(trim(coalesce(
p_payload->>'data_nascimento',
''
)), '');
v_birth := case
when v_birth_raw is null then null
when v_birth_raw ~ '^\d{4}-\d{2}-\d{2}$' then v_birth_raw::date
when v_birth_raw ~ '^\d{2}-\d{2}-\d{4}$' then to_date(v_birth_raw, 'DD-MM-YYYY')
else null
end;
insert into public.patient_intake_requests (
owner_id,
token,
status,
consent,
nome_completo,
email_principal,
telefone,
avatar_url, -- 🔥 AQUI
data_nascimento,
cpf,
rg,
genero,
estado_civil,
profissao,
escolaridade,
nacionalidade,
naturalidade,
cep,
pais,
cidade,
estado,
endereco,
numero,
complemento,
bairro,
observacoes,
notas_internas,
encaminhado_por,
onde_nos_conheceu
)
values (
v_owner_id,
p_token,
'new',
coalesce((p_payload->>'consent')::boolean, false),
nullif(trim(p_payload->>'nome_completo'), ''),
nullif(trim(p_payload->>'email_principal'), ''),
nullif(regexp_replace(coalesce(p_payload->>'telefone',''), '\D', '', 'g'), ''),
nullif(trim(p_payload->>'avatar_url'), ''), -- 🔥 AQUI
v_birth,
nullif(regexp_replace(coalesce(p_payload->>'cpf',''), '\D', '', 'g'), ''),
nullif(trim(p_payload->>'rg'), ''),
nullif(trim(p_payload->>'genero'), ''),
nullif(trim(p_payload->>'estado_civil'), ''),
nullif(trim(p_payload->>'profissao'), ''),
nullif(trim(p_payload->>'escolaridade'), ''),
nullif(trim(p_payload->>'nacionalidade'), ''),
nullif(trim(p_payload->>'naturalidade'), ''),
nullif(regexp_replace(coalesce(p_payload->>'cep',''), '\D', '', 'g'), ''),
nullif(trim(p_payload->>'pais'), ''),
nullif(trim(p_payload->>'cidade'), ''),
nullif(trim(p_payload->>'estado'), ''),
nullif(trim(p_payload->>'endereco'), ''),
nullif(trim(p_payload->>'numero'), ''),
nullif(trim(p_payload->>'complemento'), ''),
nullif(trim(p_payload->>'bairro'), ''),
nullif(trim(p_payload->>'observacoes'), ''),
nullif(trim(p_payload->>'notas_internas'), ''),
nullif(trim(p_payload->>'encaminhado_por'), ''),
nullif(trim(p_payload->>'onde_nos_conheceu'), '')
)
returning id into v_intake_id;
return v_intake_id;
end;
$_$;
ALTER FUNCTION public.create_patient_intake_request_v2(p_token text, p_payload jsonb) OWNER TO supabase_admin;
--
-- Name: create_support_session(uuid, integer); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.create_support_session(p_tenant_id uuid, p_ttl_minutes integer DEFAULT 60) RETURNS json
CREATE FUNCTION public.patients_validate_member_consistency() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_tenant_responsible uuid;
v_tenant_therapist uuid;
BEGIN
-- responsible_member sempre deve existir e ser do tenant
SELECT tenant_id INTO v_tenant_responsible
FROM public.tenant_members
WHERE id = NEW.responsible_member_id;
IF v_tenant_responsible IS NULL THEN
RAISE EXCEPTION 'Responsible member not found';
END IF;
IF NEW.tenant_id IS NULL THEN
RAISE EXCEPTION 'tenant_id is required';
END IF;
IF v_tenant_responsible <> NEW.tenant_id THEN
RAISE EXCEPTION 'Responsible member must belong to the same tenant';
END IF;
-- therapist scope: therapist_member_id deve existir e ser do mesmo tenant
IF NEW.patient_scope = 'therapist' THEN
IF NEW.therapist_member_id IS NULL THEN
RAISE EXCEPTION 'therapist_member_id is required when patient_scope=therapist';
END IF;
SELECT tenant_id INTO v_tenant_therapist
FROM public.tenant_members
WHERE id = NEW.therapist_member_id;
IF v_tenant_therapist IS NULL THEN
RAISE EXCEPTION 'Therapist member not found';
END IF;
IF v_tenant_therapist <> NEW.tenant_id THEN
RAISE EXCEPTION 'Therapist member must belong to the same tenant';
END IF;
END IF;
RETURN NEW;
END;
$$;
ALTER FUNCTION public.patients_validate_member_consistency() OWNER TO supabase_admin;
--
-- Name: patients_validate_responsible_member_tenant(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.patients_validate_responsible_member_tenant() RETURNS trigger
LANGUAGE plpgsql
AS $$
declare
m_tenant uuid;
begin
select tenant_id into m_tenant
from public.tenant_members
where id = new.responsible_member_id;
if m_tenant is null then
raise exception 'Responsible member not found';
end if;
if new.tenant_id is null then
raise exception 'tenant_id is required';
end if;
if m_tenant <> new.tenant_id then
raise exception 'Responsible member must belong to the same tenant';
end if;
return new;
end;
$$;
ALTER FUNCTION public.patients_validate_responsible_member_tenant() OWNER TO supabase_admin;
--
-- Name: populate_notification_queue(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.populate_notification_queue() RETURNS void
CREATE FUNCTION public.prevent_system_group_changes() RETURNS trigger
LANGUAGE plpgsql
AS $$
begin
-- Se for grupo do sistema, regras rígidas:
if old.is_system = true then
-- nunca pode deletar
if tg_op = 'DELETE' then
raise exception 'Grupos padrão do sistema não podem ser alterados ou excluídos.';
end if;
if tg_op = 'UPDATE' then
-- permite SOMENTE mudar tenant_id e/ou updated_at
-- qualquer mudança de conteúdo permanece proibida
if
new.nome is distinct from old.nome or
new.descricao is distinct from old.descricao or
new.cor is distinct from old.cor or
new.is_active is distinct from old.is_active or
new.is_system is distinct from old.is_system or
new.owner_id is distinct from old.owner_id or
new.therapist_id is distinct from old.therapist_id or
new.created_at is distinct from old.created_at
then
raise exception 'Grupos padrão do sistema não podem ser alterados ou excluídos.';
end if;
-- chegou aqui: só tenant_id/updated_at mudaram -> ok
return new;
end if;
end if;
-- não-system: deixa passar
if tg_op = 'DELETE' then
return old;
end if;
return new;
end;
$$;
ALTER FUNCTION public.prevent_system_group_changes() OWNER TO supabase_admin;
--
-- Name: provision_account_tenant(uuid, text, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.provision_account_tenant(p_user_id uuid, p_kind text, p_name text DEFAULT NULL::text) RETURNS uuid
CREATE FUNCTION public.rotate_patient_invite_token(p_new_token text) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
declare
v_uid uuid;
v_id uuid;
begin
-- pega o usuário logado
v_uid := auth.uid();
if v_uid is null then
raise exception 'Usuário não autenticado';
end if;
-- desativa tokens antigos ativos do usuário
update public.patient_invites
set active = false
where owner_id = v_uid
and active = true;
-- cria novo token
insert into public.patient_invites (owner_id, token, active)
values (v_uid, p_new_token, true)
returning id into v_id;
return v_id;
end;
$$;
ALTER FUNCTION public.rotate_patient_invite_token(p_new_token text) OWNER TO supabase_admin;
--
-- Name: saas_votar_doc(uuid, boolean); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.saas_votar_doc(p_doc_id uuid, p_util boolean) RETURNS jsonb
CREATE FUNCTION public.safe_delete_patient(p_patient_id uuid) RETURNS jsonb
LANGUAGE plpgsql SECURITY DEFINER
AS $$
BEGIN
-- Bloqueia se houver histórico
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;
-- Verifica ownership via RLS (owner_id ou responsible_member_id)
IF NOT EXISTS (
SELECT 1 FROM public.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 public.patients WHERE id = p_patient_id;
RETURN jsonb_build_object('ok', true);
END;
$$;
ALTER FUNCTION public.safe_delete_patient(p_patient_id uuid) OWNER TO supabase_admin;
--
-- Name: sanitize_phone_br(text); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.sanitize_phone_br(raw_phone text) RETURNS text