Sessoes 1-6 acumuladas: hardening B2, defesa em camadas, +192 testes

Repositorio estava ha ~5 sessoes sem commit. Consolida tudo desde d088a89.

Ver commit.md na raiz para descricao completa por sessao.

# Numeros
- A# auditoria abertos: 0/30
- V# verificacoes abertos: 5/52 (todos adiados com plano)
- T# testes escritos: 10/10
- Vitest: 192/192
- SQL integration: 33/33
- E2E (Playwright, novo): 5/5
- Migrations: 17 (10 novas Sessao 6)
- Areas auditadas: 7 (+documentos com 10 V#)

# Highlights Sessao 6 (hoje)
- V#34/V#41 Opcao B2: tenant_features com plano + override (RPC SECURITY DEFINER, tela /saas/tenant-features)
- A#20 rev2 self-hosted: defesa em 5 camadas (honeypot + rate limit + math captcha condicional + paranoid mode + dashboard /saas/security)
- Documentos hardening (V#43-V#49): tenant scoping em storage policies (vazamento entre clinicas eliminado), RPC validate_share_token, signatures policy granular
- SaaS Twilio Config (/saas/twilio-config): UI editavel para SID/webhook/cotacao; AUTH_TOKEN permanece em env var
- T#9 + T#10: useAgendaEvents.spec.js + Playwright E2E (descobriu bug no front que foi corrigido)

# Sessoes anteriores (1-5) consolidadas
- Sessao 1: auth/router/session, normalizeRole extraido
- Sessao 2: agenda - composables/services consolidados
- Sessao 3: pacientes - tenant_id em todas queries
- Sessao 4: security review pagina publica - 14/15 vulnerabilidades corrigidas
- Sessao 5: SaaS - P0 (A#30: 7 tabelas com RLS off corrigidas)

# .gitignore ajustado
- supabase/* + !supabase/functions/ (mantem 10 edge functions, ignora .temp/migrations gerados pelo CLI)
- database-novo/backups/ (regeneravel via db.cjs backup)
- test-results/ + playwright-report/
- .claude/settings.local.json (config local com senha de dev removida do tracking)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-04-19 15:42:46 -03:00
parent d088a89fb7
commit 7c20b518d4
175 changed files with 37325 additions and 37968 deletions
File diff suppressed because it is too large Load Diff
@@ -1,650 +0,0 @@
-- =============================================================================
-- AgenciaPsi — Functions — Agenda
-- =============================================================================
-- agenda_cfg_sync, agendador_dias_disponiveis, agendador_gerar_slug,
-- agendador_slots_disponiveis, cancel_recurrence_from,
-- cancelar_eventos_serie, fn_agenda_regras_semanais_no_overlap,
-- split_recurrence_at, sync_busy_mirror, set_updated_at_recurrence
-- =============================================================================
CREATE FUNCTION public.agenda_cfg_sync() RETURNS trigger
LANGUAGE plpgsql
AS $$
begin
if new.agenda_view_mode = 'custom' then
new.usar_horario_admin_custom := true;
new.admin_inicio_visualizacao := new.agenda_custom_start;
new.admin_fim_visualizacao := new.agenda_custom_end;
else
new.usar_horario_admin_custom := false;
end if;
return new;
end;
$$;
ALTER FUNCTION public.agenda_cfg_sync() OWNER TO supabase_admin;
--
-- Name: agendador_dias_disponiveis(text, integer, integer); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.agendador_dias_disponiveis(p_slug text, p_ano integer, p_mes integer) RETURNS TABLE(data date, tem_slots boolean)
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_owner_id uuid;
v_antecedencia int;
v_agora timestamptz;
v_data date;
v_data_inicio date;
v_data_fim date;
v_db_dow int;
v_tem_slot boolean;
v_bloqueado boolean;
BEGIN
SELECT c.owner_id, c.antecedencia_minima_horas
INTO v_owner_id, v_antecedencia
FROM public.agendador_configuracoes c
WHERE c.link_slug = p_slug AND c.ativo = true
LIMIT 1;
IF v_owner_id IS NULL THEN RETURN; END IF;
v_agora := now();
v_data_inicio := make_date(p_ano, p_mes, 1);
v_data_fim := (v_data_inicio + interval '1 month' - interval '1 day')::date;
v_data := v_data_inicio;
WHILE v_data <= v_data_fim LOOP
v_db_dow := extract(dow from v_data::timestamp)::int;
-- ── Dia inteiro bloqueado? (agenda_bloqueios) ─────────────────────────
SELECT EXISTS (
SELECT 1 FROM public.agenda_bloqueios b
WHERE b.owner_id = v_owner_id
AND b.data_inicio <= v_data
AND COALESCE(b.data_fim, v_data) >= v_data
AND b.hora_inicio IS NULL -- bloqueio de dia inteiro
AND (
(NOT b.recorrente)
OR (b.recorrente AND b.dia_semana = v_db_dow)
)
) INTO v_bloqueado;
IF v_bloqueado THEN
v_data := v_data + 1;
CONTINUE;
END IF;
-- ── Tem slots disponíveis no dia? ─────────────────────────────────────
SELECT EXISTS (
SELECT 1 FROM public.agenda_online_slots s
WHERE s.owner_id = v_owner_id
AND s.weekday = v_db_dow
AND s.enabled = true
AND (v_data::text || ' ' || s.time::text)::timestamp
AT TIME ZONE 'America/Sao_Paulo'
>= v_agora + (v_antecedencia || ' hours')::interval
) INTO v_tem_slot;
IF v_tem_slot THEN
data := v_data;
tem_slots := true;
RETURN NEXT;
END IF;
v_data := v_data + 1;
END LOOP;
END;
$$;
ALTER FUNCTION public.agendador_dias_disponiveis(p_slug text, p_ano integer, p_mes integer) OWNER TO supabase_admin;
--
-- Name: agendador_gerar_slug(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.agendador_gerar_slug() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_slug text;
v_exists boolean;
BEGIN
-- só gera se ativou e não tem slug ainda
IF NEW.ativo = true AND (NEW.link_slug IS NULL OR NEW.link_slug = '') THEN
LOOP
v_slug := lower(substring(replace(gen_random_uuid()::text, '-', ''), 1, 8));
SELECT EXISTS (
SELECT 1 FROM public.agendador_configuracoes
WHERE link_slug = v_slug AND owner_id <> NEW.owner_id
) INTO v_exists;
EXIT WHEN NOT v_exists;
END LOOP;
NEW.link_slug := v_slug;
END IF;
RETURN NEW;
END;
$$;
ALTER FUNCTION public.agendador_gerar_slug() OWNER TO supabase_admin;
--
-- Name: agendador_slots_disponiveis(text, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.agendador_slots_disponiveis(p_slug text, p_data date) RETURNS TABLE(hora time without time zone, disponivel boolean)
CREATE FUNCTION public.agendador_gerar_slug() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_slug text;
v_exists boolean;
BEGIN
-- só gera se ativou e não tem slug ainda
IF NEW.ativo = true AND (NEW.link_slug IS NULL OR NEW.link_slug = '') THEN
LOOP
v_slug := lower(substring(replace(gen_random_uuid()::text, '-', ''), 1, 8));
SELECT EXISTS (
SELECT 1 FROM public.agendador_configuracoes
WHERE link_slug = v_slug AND owner_id <> NEW.owner_id
) INTO v_exists;
EXIT WHEN NOT v_exists;
END LOOP;
NEW.link_slug := v_slug;
END IF;
RETURN NEW;
END;
$$;
ALTER FUNCTION public.agendador_gerar_slug() OWNER TO supabase_admin;
--
-- Name: agendador_slots_disponiveis(text, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.agendador_slots_disponiveis(p_slug text, p_data date) RETURNS TABLE(hora time without time zone, disponivel boolean)
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_owner_id uuid;
v_duracao int;
v_antecedencia int;
v_agora timestamptz;
v_db_dow int;
v_slot time;
v_slot_fim time;
v_slot_ts timestamptz;
v_ocupado boolean;
-- loop de recorrências
v_rule RECORD;
v_rule_start_dow int;
v_first_occ date;
v_day_diff int;
v_ex_type text;
BEGIN
SELECT c.owner_id, c.duracao_sessao_min, c.antecedencia_minima_horas
INTO v_owner_id, v_duracao, v_antecedencia
FROM public.agendador_configuracoes c
WHERE c.link_slug = p_slug AND c.ativo = true
LIMIT 1;
IF v_owner_id IS NULL THEN RETURN; END IF;
v_agora := now();
v_db_dow := extract(dow from p_data::timestamp)::int;
-- ── Dia inteiro bloqueado? (agenda_bloqueios sem hora) ───────────────────
-- Se sim, não há nenhum slot disponível — retorna vazio.
IF EXISTS (
SELECT 1 FROM public.agenda_bloqueios b
WHERE b.owner_id = v_owner_id
AND b.data_inicio <= p_data
AND COALESCE(b.data_fim, p_data) >= p_data
AND b.hora_inicio IS NULL -- bloqueio de dia inteiro
AND (
(NOT b.recorrente)
OR (b.recorrente AND b.dia_semana = v_db_dow)
)
) THEN
RETURN;
END IF;
FOR v_slot IN
SELECT s.time
FROM public.agenda_online_slots s
WHERE s.owner_id = v_owner_id
AND s.weekday = v_db_dow
AND s.enabled = true
ORDER BY s.time
LOOP
v_slot_fim := v_slot + (v_duracao || ' minutes')::interval;
v_ocupado := false;
-- ── Antecedência mínima ──────────────────────────────────────────────────
v_slot_ts := (p_data::text || ' ' || v_slot::text)::timestamp
AT TIME ZONE 'America/Sao_Paulo';
IF v_slot_ts < v_agora + (v_antecedencia || ' hours')::interval THEN
v_ocupado := true;
END IF;
-- ── Bloqueio de horário específico (agenda_bloqueios com hora) ───────────
IF NOT v_ocupado THEN
SELECT EXISTS (
SELECT 1 FROM public.agenda_bloqueios b
WHERE b.owner_id = v_owner_id
AND b.data_inicio <= p_data
AND COALESCE(b.data_fim, p_data) >= p_data
AND b.hora_inicio IS NOT NULL
AND b.hora_inicio < v_slot_fim
AND b.hora_fim > v_slot
AND (
(NOT b.recorrente)
OR (b.recorrente AND b.dia_semana = v_db_dow)
)
) INTO v_ocupado;
END IF;
-- ── Eventos avulsos internos (agenda_eventos) ────────────────────────────
IF NOT v_ocupado THEN
SELECT EXISTS (
SELECT 1 FROM public.agenda_eventos e
WHERE e.owner_id = v_owner_id
AND e.status::text NOT IN ('cancelado', 'faltou')
AND (e.inicio_em AT TIME ZONE 'America/Sao_Paulo')::date = p_data
AND (e.inicio_em AT TIME ZONE 'America/Sao_Paulo')::time < v_slot_fim
AND (e.fim_em AT TIME ZONE 'America/Sao_Paulo')::time > v_slot
) INTO v_ocupado;
END IF;
-- ── Recorrências ativas (recurrence_rules) ───────────────────────────────
IF NOT v_ocupado THEN
FOR v_rule IN
SELECT
r.id,
r.start_date::date AS start_date,
r.end_date::date AS end_date,
r.start_time::time AS start_time,
r.end_time::time AS end_time,
COALESCE(r.interval, 1)::int AS interval
FROM public.recurrence_rules r
WHERE r.owner_id = v_owner_id
AND r.status = 'ativo'
AND p_data >= r.start_date::date
AND (r.end_date IS NULL OR p_data <= r.end_date::date)
AND v_db_dow = ANY(r.weekdays)
AND r.start_time::time < v_slot_fim
AND r.end_time::time > v_slot
LOOP
v_rule_start_dow := extract(dow from v_rule.start_date)::int;
v_first_occ := v_rule.start_date
+ (((v_db_dow - v_rule_start_dow + 7) % 7))::int;
v_day_diff := (p_data - v_first_occ)::int;
IF v_day_diff >= 0 AND v_day_diff % (7 * v_rule.interval) = 0 THEN
v_ex_type := NULL;
SELECT ex.type INTO v_ex_type
FROM public.recurrence_exceptions ex
WHERE ex.recurrence_id = v_rule.id
AND ex.original_date = p_data
LIMIT 1;
IF v_ex_type IS NULL OR v_ex_type NOT IN (
'cancel_session', 'patient_missed',
'therapist_canceled', 'holiday_block',
'reschedule_session'
) THEN
v_ocupado := true;
EXIT;
END IF;
END IF;
END LOOP;
END IF;
-- ── Recorrências remarcadas para este dia ────────────────────────────────
IF NOT v_ocupado THEN
SELECT EXISTS (
SELECT 1
FROM public.recurrence_exceptions ex
JOIN public.recurrence_rules r ON r.id = ex.recurrence_id
WHERE r.owner_id = v_owner_id
AND r.status = 'ativo'
AND ex.type = 'reschedule_session'
AND ex.new_date = p_data
AND COALESCE(ex.new_start_time, r.start_time)::time < v_slot_fim
AND COALESCE(ex.new_end_time, r.end_time)::time > v_slot
) INTO v_ocupado;
END IF;
-- ── Solicitações públicas pendentes ──────────────────────────────────────
IF NOT v_ocupado THEN
SELECT EXISTS (
SELECT 1 FROM public.agendador_solicitacoes sol
WHERE sol.owner_id = v_owner_id
AND sol.status = 'pendente'
AND sol.data_solicitada = p_data
AND sol.hora_solicitada = v_slot
AND (sol.reservado_ate IS NULL OR sol.reservado_ate > v_agora)
) INTO v_ocupado;
END IF;
hora := v_slot;
disponivel := NOT v_ocupado;
RETURN NEXT;
END LOOP;
END;
$$;
ALTER FUNCTION public.agendador_slots_disponiveis(p_slug text, p_data date) OWNER TO supabase_admin;
--
-- Name: auto_create_financial_record_from_session(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.auto_create_financial_record_from_session() RETURNS trigger
CREATE FUNCTION public.cancel_recurrence_from(p_recurrence_id uuid, p_from_date date) RETURNS void
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
BEGIN
UPDATE public.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;
$$;
ALTER FUNCTION public.cancel_recurrence_from(p_recurrence_id uuid, p_from_date date) OWNER TO supabase_admin;
--
-- Name: cancel_subscription(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.cancel_subscription(p_subscription_id uuid) RETURNS public.subscriptions
CREATE FUNCTION public.cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone DEFAULT now()) RETURNS integer
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_count integer;
BEGIN
UPDATE public.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;
$$;
ALTER FUNCTION public.cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone) OWNER TO supabase_admin;
--
-- Name: FUNCTION cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone); Type: COMMENT; Schema: public; Owner: supabase_admin
--
COMMENT ON FUNCTION public.cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone) IS 'Cancela todos os eventos futuros de uma série a partir de p_a_partir_de (inclusive).
Não cancela eventos já realizados.';
--
-- Name: change_subscription_plan(uuid, uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.change_subscription_plan(p_subscription_id uuid, p_new_plan_id uuid) RETURNS public.subscriptions
CREATE FUNCTION public.fn_agenda_regras_semanais_no_overlap() RETURNS trigger
LANGUAGE plpgsql
AS $$
declare
v_count int;
begin
if new.ativo is false then
return new;
end if;
select count(*) into v_count
from public.agenda_regras_semanais r
where r.owner_id = new.owner_id
and r.dia_semana = new.dia_semana
and r.ativo is true
and (tg_op = 'INSERT' or r.id <> new.id)
and (new.hora_inicio < r.hora_fim and new.hora_fim > r.hora_inicio);
if v_count > 0 then
raise exception 'Janela sobreposta: já existe uma regra ativa nesse intervalo.';
end if;
return new;
end;
$$;
ALTER FUNCTION public.fn_agenda_regras_semanais_no_overlap() OWNER TO supabase_admin;
--
-- Name: get_financial_report(uuid, date, date, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text DEFAULT 'month'::text) 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)
CREATE FUNCTION public.set_updated_at_recurrence() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN NEW.updated_at = now(); RETURN NEW; END;
$$;
ALTER FUNCTION public.set_updated_at_recurrence() OWNER TO supabase_admin;
--
-- Name: split_recurrence_at(uuid, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.split_recurrence_at(p_recurrence_id uuid, p_from_date date) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_old public.recurrence_rules;
v_new_id uuid;
BEGIN
-- busca a regra original
SELECT * INTO v_old
FROM public.recurrence_rules
WHERE id = p_recurrence_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'recurrence_rule % não encontrada', p_recurrence_id;
END IF;
-- encerra a regra antiga na data anterior
UPDATE public.recurrence_rules
SET
end_date = p_from_date - INTERVAL '1 day',
open_ended = false,
updated_at = now()
WHERE id = p_recurrence_id;
-- cria nova regra a partir de p_from_date
INSERT INTO public.recurrence_rules (
tenant_id, 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
tenant_id, 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 public.recurrence_rules
WHERE id = p_recurrence_id
RETURNING id INTO v_new_id;
RETURN v_new_id;
END;
$$;
ALTER FUNCTION public.split_recurrence_at(p_recurrence_id uuid, p_from_date date) OWNER TO supabase_admin;
--
-- Name: subscription_intents_view_insert(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.subscription_intents_view_insert() RETURNS trigger
CREATE FUNCTION public.sync_busy_mirror_agenda_eventos() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
declare
clinic_tenant uuid;
is_personal boolean;
should_mirror boolean;
begin
-- Anti-recursão: espelho não espelha
if (tg_op <> 'DELETE') then
if new.mirror_of_event_id is not null then
return new;
end if;
else
if old.mirror_of_event_id is not null then
return old;
end if;
end if;
-- Define se é pessoal e se deve espelhar
if (tg_op = 'DELETE') then
is_personal := (old.tenant_id = old.owner_id);
should_mirror := (old.visibility_scope in ('busy_only','private'));
else
is_personal := (new.tenant_id = new.owner_id);
should_mirror := (new.visibility_scope in ('busy_only','private'));
end if;
-- Se não é pessoal, não faz nada
if not is_personal then
if (tg_op = 'DELETE') then
return old;
end if;
return new;
end if;
-- DELETE: remove espelhos existentes
if (tg_op = 'DELETE') then
delete from public.agenda_eventos e
where e.mirror_of_event_id = old.id
and e.mirror_source = 'personal_busy_mirror';
return old;
end if;
-- INSERT/UPDATE:
-- Se não deve espelhar, remove espelhos e sai
if not should_mirror then
delete from public.agenda_eventos e
where e.mirror_of_event_id = new.id
and e.mirror_source = 'personal_busy_mirror';
return new;
end if;
-- Para cada clínica onde o usuário é therapist active, cria/atualiza o "Ocupado"
for clinic_tenant in
select tm.tenant_id
from public.tenant_members tm
where tm.user_id = new.owner_id
and tm.role = 'therapist'
and tm.status = 'active'
and tm.tenant_id <> new.owner_id
loop
insert into public.agenda_eventos (
tenant_id,
owner_id,
terapeuta_id,
paciente_id,
tipo,
status,
titulo,
observacoes,
inicio_em,
fim_em,
mirror_of_event_id,
mirror_source,
visibility_scope,
created_at,
updated_at
) values (
clinic_tenant,
new.owner_id,
new.owner_id,
null,
'bloqueio'::public.tipo_evento_agenda,
'agendado'::public.status_evento_agenda,
'Ocupado',
null,
new.inicio_em,
new.fim_em,
new.id,
'personal_busy_mirror',
'public',
now(),
now()
)
on conflict (tenant_id, mirror_of_event_id) where mirror_of_event_id is not null
do update set
owner_id = excluded.owner_id,
terapeuta_id = excluded.terapeuta_id,
tipo = excluded.tipo,
status = excluded.status,
titulo = excluded.titulo,
observacoes = excluded.observacoes,
inicio_em = excluded.inicio_em,
fim_em = excluded.fim_em,
updated_at = now();
end loop;
-- Limpa espelhos de clínicas onde o vínculo therapist active não existe mais
delete from public.agenda_eventos e
where e.mirror_of_event_id = new.id
and e.mirror_source = 'personal_busy_mirror'
and not exists (
select 1
from public.tenant_members tm
where tm.user_id = new.owner_id
and tm.role = 'therapist'
and tm.status = 'active'
and tm.tenant_id = e.tenant_id
);
return new;
end;
$$;
ALTER FUNCTION public.sync_busy_mirror_agenda_eventos() OWNER TO supabase_admin;
--
-- Name: sync_overdue_financial_records(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.sync_overdue_financial_records() RETURNS integer
+3 -53
View File
@@ -1,11 +1,6 @@
-- =============================================================================
-- AgenciaPsi — Functions — auth schema
-- auth.email(), auth.jwt(), auth.role(), auth.uid()
-- =============================================================================
--
-- Name: email(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin
--
-- Functions: auth
-- Gerado automaticamente em 2026-04-17T12:23:05.221Z
-- Total: 4
CREATE FUNCTION auth.email() RETURNS text
LANGUAGE sql STABLE
@@ -17,20 +12,6 @@ CREATE FUNCTION auth.email() RETURNS text
)::text
$$;
ALTER FUNCTION auth.email() OWNER TO supabase_auth_admin;
--
-- Name: FUNCTION email(); Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
--
COMMENT ON FUNCTION auth.email() IS 'Deprecated. Use auth.jwt() -> ''email'' instead.';
--
-- Name: jwt(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin
--
CREATE FUNCTION auth.jwt() RETURNS jsonb
LANGUAGE sql STABLE
AS $$
@@ -41,13 +22,6 @@ CREATE FUNCTION auth.jwt() RETURNS jsonb
)::jsonb
$$;
ALTER FUNCTION auth.jwt() OWNER TO supabase_auth_admin;
--
-- Name: role(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin
--
CREATE FUNCTION auth.role() RETURNS text
LANGUAGE sql STABLE
AS $$
@@ -58,20 +32,6 @@ CREATE FUNCTION auth.role() RETURNS text
)::text
$$;
ALTER FUNCTION auth.role() OWNER TO supabase_auth_admin;
--
-- Name: FUNCTION role(); Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
--
COMMENT ON FUNCTION auth.role() IS 'Deprecated. Use auth.jwt() -> ''role'' instead.';
--
-- Name: uid(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin
--
CREATE FUNCTION auth.uid() RETURNS uuid
LANGUAGE sql STABLE
AS $$
@@ -81,13 +41,3 @@ CREATE FUNCTION auth.uid() RETURNS uuid
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')
)::uuid
$$;
ALTER FUNCTION auth.uid() OWNER TO supabase_auth_admin;
--
-- Name: FUNCTION uid(); Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
--
COMMENT ON FUNCTION auth.uid() IS 'Deprecated. Use auth.jwt() -> ''sub'' instead.';
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,6 @@
-- =============================================================================
-- AgenciaPsi — Functions — infraestrutura
-- extensions.grant_pg_*, pgbouncer.get_auth, etc.
-- =============================================================================
-- Functions: extensions
-- Gerado automaticamente em 2026-04-17T12:23:05.222Z
-- Total: 6
CREATE FUNCTION extensions.grant_pg_cron_access() RETURNS event_trigger
LANGUAGE plpgsql
@@ -35,20 +34,6 @@ BEGIN
END;
$$;
ALTER FUNCTION extensions.grant_pg_cron_access() OWNER TO supabase_admin;
--
-- Name: FUNCTION grant_pg_cron_access(); Type: COMMENT; Schema: extensions; Owner: supabase_admin
--
COMMENT ON FUNCTION extensions.grant_pg_cron_access() IS 'Grants access to pg_cron';
--
-- Name: grant_pg_graphql_access(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin
--
CREATE FUNCTION extensions.grant_pg_graphql_access() RETURNS event_trigger
LANGUAGE plpgsql
AS $_$
@@ -102,20 +87,6 @@ BEGIN
END;
$_$;
ALTER FUNCTION extensions.grant_pg_graphql_access() OWNER TO supabase_admin;
--
-- Name: FUNCTION grant_pg_graphql_access(); Type: COMMENT; Schema: extensions; Owner: supabase_admin
--
COMMENT ON FUNCTION extensions.grant_pg_graphql_access() IS 'Grants access to pg_graphql';
--
-- Name: grant_pg_net_access(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin
--
CREATE FUNCTION extensions.grant_pg_net_access() RETURNS event_trigger
LANGUAGE plpgsql
AS $$
@@ -145,20 +116,6 @@ BEGIN
END;
$$;
ALTER FUNCTION extensions.grant_pg_net_access() OWNER TO supabase_admin;
--
-- Name: FUNCTION grant_pg_net_access(); Type: COMMENT; Schema: extensions; Owner: supabase_admin
--
COMMENT ON FUNCTION extensions.grant_pg_net_access() IS 'Grants access to pg_net';
--
-- Name: pgrst_ddl_watch(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin
--
CREATE FUNCTION extensions.pgrst_ddl_watch() RETURNS event_trigger
LANGUAGE plpgsql
AS $$
@@ -187,13 +144,6 @@ BEGIN
END LOOP;
END; $$;
ALTER FUNCTION extensions.pgrst_ddl_watch() OWNER TO supabase_admin;
--
-- Name: pgrst_drop_watch(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin
--
CREATE FUNCTION extensions.pgrst_drop_watch() RETURNS event_trigger
LANGUAGE plpgsql
AS $$
@@ -220,13 +170,6 @@ BEGIN
END LOOP;
END; $$;
ALTER FUNCTION extensions.pgrst_drop_watch() OWNER TO supabase_admin;
--
-- Name: set_graphql_placeholder(); Type: FUNCTION; Schema: extensions; Owner: supabase_admin
--
CREATE FUNCTION extensions.set_graphql_placeholder() RETURNS event_trigger
LANGUAGE plpgsql
AS $_$
@@ -278,39 +221,3 @@ CREATE FUNCTION extensions.set_graphql_placeholder() RETURNS event_trigger
END;
$_$;
ALTER FUNCTION extensions.set_graphql_placeholder() OWNER TO supabase_admin;
--
-- Name: FUNCTION set_graphql_placeholder(); Type: COMMENT; Schema: extensions; Owner: supabase_admin
--
COMMENT ON FUNCTION extensions.set_graphql_placeholder() IS 'Reintroduces placeholder function for graphql_public.graphql';
--
-- Name: get_auth(text); Type: FUNCTION; Schema: pgbouncer; Owner: supabase_admin
--
CREATE FUNCTION pgbouncer.get_auth(p_usename text) RETURNS TABLE(username text, password text)
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO ''
AS $_$
begin
raise debug 'PgBouncer auth request: %', p_usename;
return query
select
rolname::text,
case when rolvaliduntil < now()
then null
else rolpassword::text
end
from pg_authid
where rolname=$1 and rolcanlogin;
end;
$_$;
ALTER FUNCTION pgbouncer.get_auth(p_usename text) OWNER TO supabase_admin;
@@ -1,818 +0,0 @@
-- =============================================================================
-- AgenciaPsi — Functions — Financeiro
-- =============================================================================
-- auto_create_financial_record_from_session, create_financial_record_for_session,
-- create_therapist_payout, get_financial_report, get_financial_summary,
-- list_financial_records, mark_as_paid, mark_payout_as_paid,
-- seed_default_financial_categories, sync_overdue_financial_records,
-- trg_fn_financial_records_auto_overdue, set_insurance/services_updated_at
-- =============================================================================
CREATE FUNCTION public.auto_create_financial_record_from_session() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_price NUMERIC(10,2);
v_services_total NUMERIC(10,2);
v_already_billed BOOLEAN;
BEGIN
-- ── Guards de saída rápida ──────────────────────────────────────────────
-- Só processa quando o status muda PARA 'realizado'
IF NEW.status::TEXT <> 'realizado' THEN
RETURN NEW;
END IF;
-- Só processa quando houve mudança real de status
IF OLD.status IS NOT DISTINCT FROM NEW.status THEN
RETURN NEW;
END IF;
-- Só sessões (não bloqueios, feriados, etc.)
IF NEW.tipo::TEXT <> 'sessao' THEN
RETURN NEW;
END IF;
-- Paciente obrigatório para vincular a cobrança
IF NEW.patient_id IS NULL THEN
RETURN NEW;
END IF;
-- Sessões de pacote têm cobrança gerenciada por billing_contract
IF NEW.billing_contract_id IS NOT NULL THEN
RETURN NEW;
END IF;
-- Idempotência: já existe financial_record para este evento?
SELECT billed INTO v_already_billed
FROM public.agenda_eventos
WHERE id = NEW.id;
IF v_already_billed = TRUE THEN
-- Confirma no financial_records também (dupla verificação)
IF EXISTS (
SELECT 1 FROM public.financial_records
WHERE agenda_evento_id = NEW.id AND deleted_at IS NULL
) THEN
RETURN NEW;
END IF;
END IF;
-- ── Busca do preço ──────────────────────────────────────────────────────
v_price := NULL;
-- Prioridade 1: soma dos serviços da regra de recorrência
IF NEW.recurrence_id IS NOT NULL THEN
SELECT COALESCE(SUM(rrs.final_price), 0)
INTO v_services_total
FROM public.recurrence_rule_services rrs
WHERE rrs.rule_id = NEW.recurrence_id;
IF v_services_total > 0 THEN
v_price := v_services_total;
END IF;
-- Prioridade 2: price direto da regra (fallback se sem serviços)
IF v_price IS NULL OR v_price = 0 THEN
SELECT price INTO v_price
FROM public.recurrence_rules
WHERE id = NEW.recurrence_id;
END IF;
END IF;
-- Prioridade 3: price do próprio evento de agenda
IF v_price IS NULL OR v_price = 0 THEN
v_price := NEW.price;
END IF;
-- Sem preço → não criar registro (não é erro, apenas skip silencioso)
IF v_price IS NULL OR v_price <= 0 THEN
RETURN NEW;
END IF;
-- ── Criação do financial_record ─────────────────────────────────────────
INSERT INTO public.financial_records (
owner_id,
tenant_id,
patient_id,
agenda_evento_id,
type,
amount,
discount_amount,
final_amount,
clinic_fee_pct,
clinic_fee_amount,
status,
due_date
-- payment_method: NULL até o momento do pagamento (mark_as_paid preenche)
) VALUES (
NEW.owner_id,
NEW.tenant_id,
NEW.patient_id,
NEW.id,
'receita',
v_price,
0,
v_price,
0, -- clinic_fee_pct: sem campo de configuração global no schema atual.
0, -- clinic_fee_amount: calculado manualmente ou via update posterior.
'pending',
(NEW.inicio_em::DATE + 7) -- vencimento padrão: 7 dias após a sessão
);
-- ── Marca sessão como billed ────────────────────────────────────────────
-- UPDATE em billed (não em status) → não re-dispara este trigger
UPDATE public.agenda_eventos
SET billed = TRUE
WHERE id = NEW.id;
RETURN NEW;
EXCEPTION
WHEN OTHERS THEN
-- Log silencioso: nunca bloquear a agenda por falha financeira
RAISE WARNING '[auto_create_financial_record_from_session] evento=% erro=%',
NEW.id, SQLERRM;
RETURN NEW;
END;
$$;
ALTER FUNCTION public.auto_create_financial_record_from_session() OWNER TO supabase_admin;
--
-- Name: FUNCTION auto_create_financial_record_from_session(); Type: COMMENT; Schema: public; Owner: supabase_admin
--
COMMENT ON FUNCTION public.auto_create_financial_record_from_session() IS 'Trigger que cria automaticamente um financial_record (receita, pending) quando uma sessão de agenda é marcada como realizada. Prioridade de preço: recurrence_rule_services > recurrence_rules.price > agenda_eventos.price. Skip silencioso se sem preço, pacote ou registro já existente.';
--
-- Name: can_delete_patient(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.can_delete_patient(p_patient_id uuid) RETURNS boolean
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 SETOF public.financial_records
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_existing public.financial_records%ROWTYPE;
v_new public.financial_records%ROWTYPE;
BEGIN
-- Idempotência: retorna o registro existente se já foi criado
SELECT * INTO v_existing
FROM public.financial_records
WHERE agenda_evento_id = p_agenda_evento_id
AND deleted_at IS NULL
LIMIT 1;
IF FOUND THEN
RETURN NEXT v_existing;
RETURN;
END IF;
-- Cria o novo registro
INSERT INTO public.financial_records (
tenant_id,
owner_id,
patient_id,
agenda_evento_id,
amount,
discount_amount,
final_amount,
status,
due_date
) VALUES (
p_tenant_id,
p_owner_id,
p_patient_id,
p_agenda_evento_id,
p_amount,
0,
p_amount,
'pending',
p_due_date
)
RETURNING * INTO v_new;
-- Marca o evento da agenda como billed = true
UPDATE public.agenda_eventos
SET billed = TRUE
WHERE id = p_agenda_evento_id;
RETURN NEXT v_new;
END;
$$;
ALTER 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) OWNER TO supabase_admin;
--
-- Name: create_patient_intake_request(text, text, text, text, text, boolean); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
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
CREATE FUNCTION public.create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date) RETURNS public.therapist_payouts
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_payout public.therapist_payouts%ROWTYPE;
v_total_sessions INTEGER;
v_gross NUMERIC(10,2);
v_clinic_fee NUMERIC(10,2);
v_net NUMERIC(10,2);
BEGIN
-- ── Verificação de permissão ────────────────────────────────────────────
-- Apenas o próprio terapeuta ou o tenant_admin pode criar o repasse
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;
-- ── Verifica se já existe repasse para o mesmo período ─────────────────
IF EXISTS (
SELECT 1 FROM public.therapist_payouts
WHERE owner_id = p_therapist_id
AND tenant_id = p_tenant_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;
-- ── Agrega os financial_records elegíveis ──────────────────────────────
-- Elegíveis: paid, receita, owner=terapeuta, tenant correto, paid_at no período,
-- não soft-deleted, ainda não vinculados a nenhum payout.
SELECT
COUNT(*) AS total_sessions,
COALESCE(SUM(amount), 0) AS gross_amount,
COALESCE(SUM(clinic_fee_amount), 0) AS clinic_fee_total,
COALESCE(SUM(net_amount), 0) AS net_amount
INTO
v_total_sessions, v_gross, v_clinic_fee, v_net
FROM public.financial_records fr
WHERE fr.owner_id = p_therapist_id
AND fr.tenant_id = p_tenant_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 public.therapist_payout_records tpr
WHERE tpr.financial_record_id = fr.id
);
-- Sem registros elegíveis → não criar payout vazio
IF v_total_sessions = 0 THEN
RAISE EXCEPTION
'Nenhum registro financeiro elegível encontrado para o período % a %.',
p_period_start, p_period_end;
END IF;
-- ── Cria o repasse ─────────────────────────────────────────────────────
INSERT INTO public.therapist_payouts (
owner_id,
tenant_id,
period_start,
period_end,
total_sessions,
gross_amount,
clinic_fee_total,
net_amount,
status
) VALUES (
p_therapist_id,
p_tenant_id,
p_period_start,
p_period_end,
v_total_sessions,
v_gross,
v_clinic_fee,
v_net,
'pending'
)
RETURNING * INTO v_payout;
-- ── Vincula os financial_records ao repasse ────────────────────────────
INSERT INTO public.therapist_payout_records (payout_id, financial_record_id)
SELECT v_payout.id, fr.id
FROM public.financial_records fr
WHERE fr.owner_id = p_therapist_id
AND fr.tenant_id = p_tenant_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 public.therapist_payout_records tpr
WHERE tpr.financial_record_id = fr.id
);
RETURN v_payout;
END;
$$;
ALTER FUNCTION public.create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date) OWNER TO supabase_admin;
--
-- Name: FUNCTION create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date); Type: COMMENT; Schema: public; Owner: supabase_admin
--
COMMENT ON FUNCTION public.create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date) IS 'Cria um repasse para o terapeuta com todos os financial_records paid+receita do período que ainda não estejam vinculados a outro repasse. Lança exceção se não houver registros elegíveis ou se já houver repasse ativo no período.';
--
-- Name: current_member_id(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.current_member_id(p_tenant_id uuid) RETURNS uuid
CREATE FUNCTION public.get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text DEFAULT 'month'::text) 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 sql STABLE SECURITY DEFINER
SET search_path TO 'public'
AS $$
-- ── Valida p_group_by antes de executar ──────────────────────────────────
-- (lança erro se valor inválido; plpgsql seria necessário para isso em SQL puro,
-- então usamos um CTE de validação com CASE WHEN para retornar vazio em vez de erro)
WITH base AS (
SELECT
fr.type,
fr.amount,
fr.final_amount,
fr.status,
fr.deleted_at,
-- Chave de agrupamento calculada conforme p_group_by
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 -- group_by inválido → group_key NULL → retorno vazio
END AS gkey,
-- Label legível (enriquecido via JOIN abaixo quando possível)
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 public.financial_records fr
LEFT JOIN public.financial_categories fc
ON fc.id = fr.category_id
LEFT JOIN public.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 AS group_key,
glabel AS group_label,
COALESCE(SUM(final_amount) FILTER (WHERE type = 'receita' AND status = 'paid'), 0)
AS total_receitas,
COALESCE(SUM(final_amount) FILTER (WHERE type = 'despesa' AND status = 'paid'), 0)
AS total_despesas,
COALESCE(SUM(final_amount) FILTER (WHERE type = 'receita' AND status = 'paid'), 0)
- COALESCE(SUM(final_amount) FILTER (WHERE type = 'despesa' AND status = 'paid'), 0)
AS saldo,
COALESCE(SUM(final_amount) FILTER (WHERE status = 'pending'), 0) AS total_pendente,
COALESCE(SUM(final_amount) FILTER (WHERE status = 'overdue'), 0) AS total_overdue,
COUNT(*) AS count_records
FROM base
WHERE gkey IS NOT NULL -- descarta p_group_by inválido
GROUP BY gkey, glabel
ORDER BY gkey ASC;
$$;
ALTER FUNCTION public.get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text) OWNER TO supabase_admin;
--
-- Name: FUNCTION get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text); Type: COMMENT; Schema: public; Owner: supabase_admin
--
COMMENT ON FUNCTION public.get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text) IS 'Relatório financeiro agrupado por mês, semana ISO, categoria ou paciente. p_group_by aceita: ''month'' | ''week'' | ''category'' | ''patient''. Totais de receita/despesa consideram apenas registros com status=paid. total_pendente e total_overdue incluem todos os tipos (receita + despesa).';
--
-- Name: get_financial_summary(uuid, integer, integer); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.get_financial_summary(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 sql STABLE SECURITY DEFINER
SET search_path TO 'public'
AS $$
SELECT
-- Receitas pagas no período
COALESCE(SUM(amount) FILTER (
WHERE type = 'receita' AND status = 'paid'
), 0) AS total_receitas,
-- Despesas pagas no período
COALESCE(SUM(amount) FILTER (
WHERE type = 'despesa' AND status = 'paid'
), 0) AS total_despesas,
-- Tudo pendente ou vencido (receitas + despesas)
COALESCE(SUM(amount) FILTER (
WHERE status IN ('pending', 'overdue')
), 0) AS total_pendente,
-- Saldo líquido (receitas pagas despesas pagas)
COALESCE(SUM(amount) FILTER (
WHERE type = 'receita' AND status = 'paid'
), 0)
- COALESCE(SUM(amount) FILTER (
WHERE type = 'despesa' AND status = 'paid'
), 0) AS saldo_liquido,
-- Total repassado à clínica (apenas receitas pagas)
COALESCE(SUM(clinic_fee_amount) FILTER (
WHERE type = 'receita' AND status = 'paid'
), 0) AS total_repasse,
-- Contadores (excluindo soft-deleted)
COUNT(*) FILTER (WHERE type = 'receita' AND deleted_at IS NULL) AS count_receitas,
COUNT(*) FILTER (WHERE type = 'despesa' AND deleted_at IS NULL) AS count_despesas
FROM public.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;
$$;
ALTER FUNCTION public.get_financial_summary(p_owner_id uuid, p_year integer, p_month integer) OWNER TO supabase_admin;
--
-- Name: get_my_email(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.get_my_email() RETURNS text
CREATE FUNCTION public.list_financial_records(p_owner_id uuid, p_year integer DEFAULT NULL::integer, p_month integer DEFAULT NULL::integer, p_type text DEFAULT NULL::text, p_status text DEFAULT NULL::text, p_patient_id uuid DEFAULT NULL::uuid, p_limit integer DEFAULT 50, p_offset integer DEFAULT 0) RETURNS SETOF public.financial_records
LANGUAGE sql STABLE SECURITY DEFINER
SET search_path TO 'public'
AS $$
SELECT *
FROM public.financial_records
WHERE owner_id = p_owner_id
AND deleted_at IS NULL
AND (p_type IS NULL OR type::TEXT = p_type)
AND (p_status IS NULL OR status = p_status)
AND (p_patient_id IS NULL OR patient_id = p_patient_id)
AND (p_year IS NULL OR EXTRACT(YEAR FROM COALESCE(paid_at::DATE, due_date, created_at::DATE)) = p_year)
AND (p_month IS NULL OR EXTRACT(MONTH FROM COALESCE(paid_at::DATE, due_date, created_at::DATE)) = p_month)
ORDER BY COALESCE(paid_at, due_date::TIMESTAMPTZ, created_at) DESC
LIMIT p_limit
OFFSET p_offset;
$$;
ALTER FUNCTION public.list_financial_records(p_owner_id uuid, p_year integer, p_month integer, p_type text, p_status text, p_patient_id uuid, p_limit integer, p_offset integer) OWNER TO supabase_admin;
--
-- Name: mark_as_paid(uuid, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.mark_as_paid(p_financial_record_id uuid, p_payment_method text) RETURNS SETOF public.financial_records
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_record public.financial_records%ROWTYPE;
BEGIN
-- Garante que o registro pertence ao usuário autenticado (RLS não aplica em SECURITY DEFINER)
SELECT * INTO v_record
FROM public.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 public.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 NEXT v_record;
END;
$$;
ALTER FUNCTION public.mark_as_paid(p_financial_record_id uuid, p_payment_method text) OWNER TO supabase_admin;
--
-- Name: mark_payout_as_paid(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.mark_payout_as_paid(p_payout_id uuid) RETURNS public.therapist_payouts
CREATE FUNCTION public.mark_payout_as_paid(p_payout_id uuid) RETURNS public.therapist_payouts
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_payout public.therapist_payouts%ROWTYPE;
BEGIN
-- Busca o payout
SELECT * INTO v_payout
FROM public.therapist_payouts
WHERE id = p_payout_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Repasse não encontrado: %', p_payout_id;
END IF;
-- Verifica permissão: apenas tenant_admin do tenant do repasse
IF NOT public.is_tenant_admin(v_payout.tenant_id) THEN
RAISE EXCEPTION 'Apenas o administrador da clínica pode marcar repasses como pagos.';
END IF;
-- Verifica status
IF v_payout.status <> 'pending' THEN
RAISE EXCEPTION
'Repasse já está com status ''%''. Apenas repasses pendentes podem ser pagos.',
v_payout.status;
END IF;
-- Atualiza
UPDATE public.therapist_payouts
SET
status = 'paid',
paid_at = NOW(),
updated_at = NOW()
WHERE id = p_payout_id
RETURNING * INTO v_payout;
RETURN v_payout;
END;
$$;
ALTER FUNCTION public.mark_payout_as_paid(p_payout_id uuid) OWNER TO supabase_admin;
--
-- Name: FUNCTION mark_payout_as_paid(p_payout_id uuid); Type: COMMENT; Schema: public; Owner: supabase_admin
--
COMMENT ON FUNCTION public.mark_payout_as_paid(p_payout_id uuid) IS 'Marca um repasse de terapeuta como pago. Apenas o tenant_admin pode chamar. Apenas repasses com status=pending podem ser finalizados.';
--
-- Name: my_tenants(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.my_tenants() RETURNS TABLE(tenant_id uuid, role text, status text, kind text)
CREATE FUNCTION public.seed_default_financial_categories(p_user_id uuid) RETURNS void
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
BEGIN
INSERT INTO public.financial_categories (user_id, name, type, color, icon, sort_order)
VALUES
(p_user_id, 'Sessão', 'receita', '#22c55e', 'pi pi-heart', 1),
(p_user_id, 'Supervisão', 'receita', '#6366f1', 'pi pi-users', 2),
(p_user_id, 'Convênio', 'receita', '#3b82f6', 'pi pi-building', 3),
(p_user_id, 'Grupo terapêutico', 'receita', '#f59e0b', 'pi pi-sitemap', 4),
(p_user_id, 'Outro (receita)', 'receita', '#8b5cf6', 'pi pi-plus-circle', 5),
(p_user_id, 'Aluguel sala', 'despesa', '#ef4444', 'pi pi-home', 1),
(p_user_id, 'Plataforma/SaaS', 'despesa', '#f97316', 'pi pi-desktop', 2),
(p_user_id, 'Repasse clínica', 'despesa', '#64748b', 'pi pi-arrow-right-arrow-left', 3),
(p_user_id, 'Supervisão (custo)', 'despesa', '#6366f1', 'pi pi-users', 4),
(p_user_id, 'Outro (despesa)', 'despesa', '#94a3b8', 'pi pi-minus-circle', 5)
ON CONFLICT DO NOTHING;
END;
$$;
ALTER FUNCTION public.seed_default_financial_categories(p_user_id uuid) OWNER TO supabase_admin;
--
-- Name: seed_determined_commitments(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.seed_determined_commitments(p_tenant_id uuid) RETURNS void
CREATE FUNCTION public.set_insurance_plans_updated_at() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN NEW.updated_at = now(); RETURN NEW; END; $$;
ALTER FUNCTION public.set_insurance_plans_updated_at() OWNER TO supabase_admin;
--
-- Name: set_owner_id(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.set_owner_id() RETURNS trigger
LANGUAGE plpgsql
AS $$
begin
if new.owner_id is null then
new.owner_id := auth.uid();
end if;
return new;
end;
$$;
ALTER FUNCTION public.set_owner_id() OWNER TO supabase_admin;
--
-- Name: set_services_updated_at(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.set_services_updated_at() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$;
ALTER FUNCTION public.set_services_updated_at() OWNER TO supabase_admin;
--
-- Name: set_tenant_feature_exception(uuid, text, boolean, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.set_tenant_feature_exception(p_tenant_id uuid, p_feature_key text, p_enabled boolean, p_reason text DEFAULT NULL::text) RETURNS void
CREATE FUNCTION public.set_updated_at() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$;
ALTER FUNCTION public.set_updated_at() OWNER TO supabase_admin;
--
-- Name: set_updated_at_recurrence(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.set_updated_at_recurrence() RETURNS trigger
CREATE FUNCTION public.sync_overdue_financial_records() RETURNS integer
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_count integer;
BEGIN
UPDATE public.financial_records
SET
status = 'overdue',
updated_at = NOW()
WHERE status = 'pending'
AND due_date IS NOT NULL
AND due_date < CURRENT_DATE
AND deleted_at IS NULL;
GET DIAGNOSTICS v_count = ROW_COUNT;
RETURN v_count;
END;
$$;
ALTER FUNCTION public.sync_overdue_financial_records() OWNER TO supabase_admin;
--
-- Name: FUNCTION sync_overdue_financial_records(); Type: COMMENT; Schema: public; Owner: supabase_admin
--
COMMENT ON FUNCTION public.sync_overdue_financial_records() IS 'Marca como overdue todos os financial_records pendentes com due_date vencido. Pode ser chamada manualmente, via pg_cron ou via Supabase Edge Function agendada.';
--
-- Name: tenant_accept_invite(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.tenant_accept_invite(p_token uuid) RETURNS jsonb
CREATE FUNCTION public.trg_fn_financial_records_auto_overdue() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
BEGIN
IF NEW.status = 'pending'
AND NEW.due_date IS NOT NULL
AND NEW.due_date < CURRENT_DATE
THEN
NEW.status := 'overdue';
END IF;
RETURN NEW;
END;
$$;
ALTER FUNCTION public.trg_fn_financial_records_auto_overdue() OWNER TO supabase_admin;
--
-- Name: unstick_notification_queue(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.unstick_notification_queue() RETURNS integer
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_unstuck integer;
BEGIN
UPDATE public.notification_queue
SET status = 'pendente',
attempts = attempts + 1,
last_error = 'Timeout: preso em processando por >10min',
next_retry_at = now() + interval '2 minutes'
WHERE status = 'processando'
AND updated_at < now() - interval '10 minutes';
GET DIAGNOSTICS v_unstuck = ROW_COUNT;
RETURN v_unstuck;
END;
$$;
ALTER FUNCTION public.unstick_notification_queue() OWNER TO supabase_admin;
--
-- Name: update_payment_settings_updated_at(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.update_payment_settings_updated_at() RETURNS trigger
CREATE FUNCTION public.update_payment_settings_updated_at() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$;
ALTER FUNCTION public.update_payment_settings_updated_at() OWNER TO supabase_admin;
--
-- Name: update_professional_pricing_updated_at(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.update_professional_pricing_updated_at() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$;
ALTER FUNCTION public.update_professional_pricing_updated_at() OWNER TO supabase_admin;
--
-- Name: user_has_feature(uuid, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.user_has_feature(_user_id uuid, _feature text) RETURNS boolean
-776
View File
@@ -1,776 +0,0 @@
-- =============================================================================
-- AgenciaPsi — Functions — Compromissos, Suporte, SaaS
-- =============================================================================
-- seed_determined_commitments, delete_commitment_full,
-- delete_determined_commitment, guard_locked_commitment,
-- create_support_session, revoke_support_session, validate_support_session,
-- saas_votar_doc, faq_votar, notice_track_click/view,
-- sanitize_phone_br, create_clinic_tenant
-- =============================================================================
CREATE FUNCTION public.create_clinic_tenant(p_name text) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
AS $$
declare
v_uid uuid;
v_tenant uuid;
v_name text;
begin
v_uid := auth.uid();
if v_uid is null then
raise exception 'Not authenticated';
end if;
v_name := nullif(trim(coalesce(p_name, '')), '');
if v_name is null then
v_name := 'Clínica';
end if;
insert into public.tenants (name, kind, created_at)
values (v_name, 'clinic', now())
returning id into v_tenant;
insert into public.tenant_members (tenant_id, user_id, role, status, created_at)
values (v_tenant, v_uid, 'tenant_admin', 'active', now());
return v_tenant;
end;
$$;
ALTER FUNCTION public.create_clinic_tenant(p_name text) OWNER TO supabase_admin;
--
-- Name: financial_records; Type: TABLE; Schema: public; Owner: supabase_admin
--
CREATE TABLE public.financial_records (
id uuid DEFAULT gen_random_uuid() NOT NULL,
owner_id uuid NOT NULL,
tenant_id uuid,
type public.financial_record_type DEFAULT 'receita'::public.financial_record_type NOT NULL,
amount numeric(10,2) NOT NULL,
description text,
category text,
payment_method text,
paid_at timestamp with time zone,
due_date date,
installments smallint DEFAULT 1,
installment_number smallint DEFAULT 1,
installment_group uuid,
agenda_evento_id uuid,
patient_id uuid,
clinic_fee_pct numeric(5,2) DEFAULT 0,
clinic_fee_amount numeric(10,2) DEFAULT 0,
net_amount numeric(10,2) GENERATED ALWAYS AS ((amount - clinic_fee_amount)) STORED,
insurance_plan_id uuid,
notes text,
tags text[],
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
deleted_at timestamp with time zone,
discount_amount numeric(10,2) DEFAULT 0 NOT NULL,
final_amount numeric(10,2) DEFAULT 0 NOT NULL,
status text DEFAULT 'pending'::text NOT NULL,
category_id uuid,
CONSTRAINT financial_records_amount_check CHECK ((amount >= (0)::numeric)),
CONSTRAINT financial_records_clinic_fee_amount_check CHECK ((clinic_fee_amount >= (0)::numeric)),
CONSTRAINT financial_records_clinic_fee_pct_check CHECK (((clinic_fee_pct >= (0)::numeric) AND (clinic_fee_pct <= (100)::numeric))),
CONSTRAINT financial_records_discount_amount_check CHECK ((discount_amount >= (0)::numeric)),
CONSTRAINT financial_records_final_amount_check CHECK ((final_amount >= (0)::numeric)),
CONSTRAINT financial_records_installments_check CHECK ((installments >= 1)),
CONSTRAINT financial_records_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'paid'::text, 'partial'::text, 'overdue'::text, 'cancelled'::text, 'refunded'::text])))
);
ALTER TABLE public.financial_records OWNER TO supabase_admin;
--
-- Name: create_financial_record_for_session(uuid, uuid, uuid, uuid, numeric, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
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 SETOF public.financial_records
CREATE FUNCTION public.create_support_session(p_tenant_id uuid, p_ttl_minutes integer DEFAULT 60) RETURNS json
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_admin_id uuid;
v_role text;
v_token text;
v_expires timestamp with time zone;
v_session support_sessions;
BEGIN
-- Verifica autenticação
v_admin_id := auth.uid();
IF v_admin_id IS NULL THEN
RAISE EXCEPTION 'Não autenticado.' USING ERRCODE = 'P0001';
END IF;
-- Verifica role saas_admin
SELECT role INTO v_role
FROM public.profiles
WHERE id = v_admin_id;
IF v_role <> 'saas_admin' THEN
RAISE EXCEPTION 'Acesso negado. Somente saas_admin pode criar sessões de suporte.'
USING ERRCODE = 'P0002';
END IF;
-- Valida TTL (1 a 120 minutos)
IF p_ttl_minutes < 1 OR p_ttl_minutes > 120 THEN
RAISE EXCEPTION 'TTL inválido. Use entre 1 e 120 minutos.'
USING ERRCODE = 'P0003';
END IF;
-- Valida tenant
IF NOT EXISTS (SELECT 1 FROM public.tenants WHERE id = p_tenant_id) THEN
RAISE EXCEPTION 'Tenant não encontrado.'
USING ERRCODE = 'P0004';
END IF;
-- Gera token único (64 chars hex, sem pgcrypto)
v_token := replace(gen_random_uuid()::text, '-', '') || replace(gen_random_uuid()::text, '-', '');
v_expires := now() + (p_ttl_minutes || ' minutes')::interval;
-- Insere sessão
INSERT INTO public.support_sessions (tenant_id, admin_id, token, expires_at)
VALUES (p_tenant_id, v_admin_id, v_token, v_expires)
RETURNING * INTO v_session;
RETURN json_build_object(
'token', v_session.token,
'expires_at', v_session.expires_at,
'session_id', v_session.id
);
END;
$$;
ALTER FUNCTION public.create_support_session(p_tenant_id uuid, p_ttl_minutes integer) OWNER TO supabase_admin;
--
-- Name: therapist_payouts; Type: TABLE; Schema: public; Owner: supabase_admin
--
CREATE TABLE public.therapist_payouts (
id uuid DEFAULT gen_random_uuid() NOT NULL,
owner_id uuid NOT NULL,
tenant_id uuid NOT NULL,
period_start date NOT NULL,
period_end date NOT NULL,
total_sessions integer DEFAULT 0 NOT NULL,
gross_amount numeric(10,2) DEFAULT 0 NOT NULL,
clinic_fee_total numeric(10,2) DEFAULT 0 NOT NULL,
net_amount numeric(10,2) DEFAULT 0 NOT NULL,
status text DEFAULT 'pending'::text NOT NULL,
paid_at timestamp with time zone,
notes text,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT therapist_payouts_clinic_fee_total_check CHECK ((clinic_fee_total >= (0)::numeric)),
CONSTRAINT therapist_payouts_gross_amount_check CHECK ((gross_amount >= (0)::numeric)),
CONSTRAINT therapist_payouts_net_amount_check CHECK ((net_amount >= (0)::numeric)),
CONSTRAINT therapist_payouts_period_chk CHECK ((period_end >= period_start)),
CONSTRAINT therapist_payouts_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'paid'::text, 'cancelled'::text])))
);
ALTER TABLE public.therapist_payouts OWNER TO supabase_admin;
--
-- Name: create_therapist_payout(uuid, uuid, date, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date) RETURNS public.therapist_payouts
CREATE FUNCTION public.delete_commitment_full(p_tenant_id uuid, p_commitment_id uuid) RETURNS jsonb
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
declare
v_is_native boolean;
v_fields int := 0;
v_logs int := 0;
v_parent int := 0;
begin
if auth.uid() is null then
raise exception 'Not authenticated';
end if;
if not exists (
select 1
from public.tenant_members tm
where tm.tenant_id = p_tenant_id
and tm.user_id = auth.uid()
and tm.status = 'active'
) then
raise exception 'Not allowed';
end if;
select dc.is_native
into v_is_native
from public.determined_commitments dc
where dc.tenant_id = p_tenant_id
and 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 public.determined_commitment_fields
where tenant_id = p_tenant_id
and commitment_id = p_commitment_id;
get diagnostics v_fields = row_count;
delete from public.commitment_time_logs
where tenant_id = p_tenant_id
and commitment_id = p_commitment_id;
get diagnostics v_logs = row_count;
delete from public.determined_commitments
where tenant_id = p_tenant_id
and id = p_commitment_id;
get diagnostics v_parent = row_count;
if v_parent <> 1 then
raise exception 'Parent not deleted (RLS/owner issue).';
end if;
return jsonb_build_object(
'ok', true,
'deleted', jsonb_build_object(
'fields', v_fields,
'logs', v_logs,
'commitment', v_parent
)
);
end;
$$;
ALTER FUNCTION public.delete_commitment_full(p_tenant_id uuid, p_commitment_id uuid) OWNER TO postgres;
--
-- Name: delete_determined_commitment(uuid, uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.delete_determined_commitment(p_tenant_id uuid, p_commitment_id uuid) RETURNS jsonb
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
declare
v_is_native boolean;
v_fields_deleted int := 0;
v_logs_deleted int := 0;
v_commitment_deleted int := 0;
begin
if auth.uid() is null then
raise exception 'Not authenticated';
end if;
if not exists (
select 1
from public.tenant_members tm
where tm.tenant_id = p_tenant_id
and tm.user_id = auth.uid()
and tm.status = 'active'
) then
raise exception 'Not allowed';
end if;
select dc.is_native
into v_is_native
from public.determined_commitments dc
where dc.tenant_id = p_tenant_id
and 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 public.determined_commitment_fields f
where f.tenant_id = p_tenant_id
and f.commitment_id = p_commitment_id;
get diagnostics v_fields_deleted = row_count;
delete from public.commitment_time_logs l
where l.tenant_id = p_tenant_id
and l.commitment_id = p_commitment_id;
get diagnostics v_logs_deleted = row_count;
delete from public.determined_commitments dc
where dc.tenant_id = p_tenant_id
and dc.id = p_commitment_id;
get diagnostics v_commitment_deleted = row_count;
if v_commitment_deleted <> 1 then
raise exception 'Delete did not remove the commitment (tenant mismatch?)';
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_deleted,
'logs', v_logs_deleted,
'commitment', v_commitment_deleted
)
);
end;
$$;
ALTER FUNCTION public.delete_determined_commitment(p_tenant_id uuid, p_commitment_id uuid) OWNER TO supabase_admin;
--
-- Name: dev_list_auth_users(integer); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.dev_list_auth_users(p_limit integer DEFAULT 50) RETURNS TABLE(id uuid, email text, created_at timestamp with time zone)
CREATE FUNCTION public.faq_votar(faq_id uuid) RETURNS void
LANGUAGE sql SECURITY DEFINER
AS $$
update public.saas_faq
set votos = votos + 1,
updated_at = now()
where id = faq_id
and ativo = true;
$$;
ALTER FUNCTION public.faq_votar(faq_id uuid) OWNER TO supabase_admin;
--
-- Name: fix_all_subscription_mismatches(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.fix_all_subscription_mismatches() RETURNS void
LANGUAGE plpgsql SECURITY DEFINER
AS $$
declare
r record;
begin
for r in
select distinct s.user_id as owner_id
from public.subscriptions s
where s.status = 'active'
and s.user_id is not null
loop
perform public.rebuild_owner_entitlements(r.owner_id);
end loop;
end;
$$;
ALTER FUNCTION public.fix_all_subscription_mismatches() OWNER TO supabase_admin;
--
-- Name: fn_agenda_regras_semanais_no_overlap(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.fn_agenda_regras_semanais_no_overlap() RETURNS trigger
CREATE FUNCTION public.guard_locked_commitment() RETURNS trigger
LANGUAGE plpgsql
AS $$
begin
if (old.is_locked = true) then
if (tg_op = 'DELETE') then
raise exception 'Compromisso bloqueado não pode ser excluído.';
end if;
if (tg_op = 'UPDATE') then
if (new.active = false) then
raise exception 'Compromisso bloqueado não pode ser desativado.';
end if;
-- trava renomear (mantém o "Sessão" sempre igual)
if (new.name is distinct from old.name) then
raise exception 'Compromisso bloqueado não pode ser renomeado.';
end if;
-- se quiser travar descrição também, descomente:
-- if (new.description is distinct from old.description) then
-- raise exception 'Compromisso bloqueado não pode alterar descrição.';
-- end if;
end if;
end if;
return new;
end;
$$;
ALTER FUNCTION public.guard_locked_commitment() OWNER TO supabase_admin;
--
-- Name: guard_no_change_core_plan_key(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.guard_no_change_core_plan_key() RETURNS trigger
CREATE FUNCTION public.notice_track_click(p_notice_id uuid) RETURNS void
LANGUAGE plpgsql SECURITY DEFINER
AS $$
begin
update public.global_notices
set clicks_count = clicks_count + 1
where id = p_notice_id;
end;
$$;
ALTER FUNCTION public.notice_track_click(p_notice_id uuid) OWNER TO supabase_admin;
--
-- Name: notice_track_view(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.notice_track_view(p_notice_id uuid) RETURNS void
LANGUAGE plpgsql SECURITY DEFINER
AS $$
begin
update public.global_notices
set views_count = views_count + 1
where id = p_notice_id;
end;
$$;
ALTER FUNCTION public.notice_track_view(p_notice_id uuid) OWNER TO supabase_admin;
--
-- Name: notify_on_intake(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.notify_on_intake() RETURNS trigger
CREATE FUNCTION public.revoke_support_session(p_token text) RETURNS boolean
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_admin_id uuid;
v_role text;
BEGIN
v_admin_id := auth.uid();
IF v_admin_id IS NULL THEN
RAISE EXCEPTION 'Não autenticado.' USING ERRCODE = 'P0001';
END IF;
SELECT role INTO v_role FROM public.profiles WHERE id = v_admin_id;
IF v_role <> 'saas_admin' THEN
RAISE EXCEPTION 'Acesso negado.' USING ERRCODE = 'P0002';
END IF;
DELETE FROM public.support_sessions
WHERE token = p_token
AND admin_id = v_admin_id;
RETURN FOUND;
END;
$$;
ALTER FUNCTION public.revoke_support_session(p_token text) OWNER TO supabase_admin;
--
-- Name: rotate_patient_invite_token(text); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.rotate_patient_invite_token(p_new_token text) RETURNS uuid
CREATE FUNCTION public.saas_votar_doc(p_doc_id uuid, p_util boolean) RETURNS jsonb
LANGUAGE plpgsql SECURITY DEFINER
AS $$
declare
v_uid uuid := auth.uid();
v_voto_antigo boolean;
begin
if v_uid is null then
raise exception 'Não autenticado';
end if;
-- Verifica se já votou
select util into v_voto_antigo
from public.saas_doc_votos
where doc_id = p_doc_id and user_id = v_uid;
if found then
-- Já votou igual → cancela o voto (toggle)
if v_voto_antigo = p_util then
delete from public.saas_doc_votos
where doc_id = p_doc_id and user_id = v_uid;
update public.saas_docs set
votos_util = greatest(0, votos_util - (case when p_util then 1 else 0 end)),
votos_nao_util = greatest(0, votos_nao_util - (case when not p_util then 1 else 0 end)),
updated_at = now()
where id = p_doc_id;
return jsonb_build_object('acao', 'removido', 'util', null);
else
-- Mudou de voto
update public.saas_doc_votos set util = p_util, updated_at = now()
where doc_id = p_doc_id and user_id = v_uid;
update public.saas_docs set
votos_util = greatest(0, votos_util + (case when p_util then 1 else -1 end)),
votos_nao_util = greatest(0, votos_nao_util + (case when not p_util then 1 else -1 end)),
updated_at = now()
where id = p_doc_id;
return jsonb_build_object('acao', 'atualizado', 'util', p_util);
end if;
else
-- Primeiro voto
insert into public.saas_doc_votos (doc_id, user_id, util)
values (p_doc_id, v_uid, p_util);
update public.saas_docs set
votos_util = votos_util + (case when p_util then 1 else 0 end),
votos_nao_util = votos_nao_util + (case when not p_util then 1 else 0 end),
updated_at = now()
where id = p_doc_id;
return jsonb_build_object('acao', 'registrado', 'util', p_util);
end if;
end;
$$;
ALTER FUNCTION public.saas_votar_doc(p_doc_id uuid, p_util boolean) OWNER TO supabase_admin;
--
-- Name: safe_delete_patient(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.safe_delete_patient(p_patient_id uuid) RETURNS jsonb
CREATE FUNCTION public.sanitize_phone_br(raw_phone text) RETURNS text
LANGUAGE plpgsql IMMUTABLE
AS $$ DECLARE digits text;
BEGIN
digits := regexp_replace(COALESCE(raw_phone, ''), '[^0-9]', '', 'g');
IF digits = '' THEN RETURN ''; END IF;
IF length(digits) = 10 OR length(digits) = 11 THEN
digits := '55' || digits;
END IF;
RETURN digits;
END; $$;
ALTER FUNCTION public.sanitize_phone_br(raw_phone text) OWNER TO supabase_admin;
--
-- Name: seed_default_financial_categories(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.seed_default_financial_categories(p_user_id uuid) RETURNS void
CREATE FUNCTION public.seed_determined_commitments(p_tenant_id uuid) RETURNS void
LANGUAGE plpgsql SECURITY DEFINER
AS $$
declare
v_id uuid;
begin
-- Sessão (locked + sempre ativa)
if not exists (
select 1 from public.determined_commitments
where tenant_id = p_tenant_id and is_native = true and native_key = 'session'
) then
insert into public.determined_commitments
(tenant_id, is_native, native_key, is_locked, active, name, description)
values
(p_tenant_id, true, 'session', true, true, 'Sessão', 'Sessão com paciente');
end if;
-- Leitura
if not exists (
select 1 from public.determined_commitments
where tenant_id = p_tenant_id and is_native = true and native_key = 'reading'
) then
insert into public.determined_commitments
(tenant_id, is_native, native_key, is_locked, active, name, description)
values
(p_tenant_id, true, 'reading', false, true, 'Leitura', 'Praticar leitura');
end if;
-- Supervisão
if not exists (
select 1 from public.determined_commitments
where tenant_id = p_tenant_id and is_native = true and native_key = 'supervision'
) then
insert into public.determined_commitments
(tenant_id, is_native, native_key, is_locked, active, name, description)
values
(p_tenant_id, true, 'supervision', false, true, 'Supervisão', 'Supervisão');
end if;
-- Aula ✅ (corrigido)
if not exists (
select 1 from public.determined_commitments
where tenant_id = p_tenant_id and is_native = true and native_key = 'class'
) then
insert into public.determined_commitments
(tenant_id, is_native, native_key, is_locked, active, name, description)
values
(p_tenant_id, true, 'class', false, false, 'Aula', 'Dar aula');
end if;
-- Análise pessoal
if not exists (
select 1 from public.determined_commitments
where tenant_id = p_tenant_id and is_native = true and native_key = 'analysis'
) then
insert into public.determined_commitments
(tenant_id, is_native, native_key, is_locked, active, name, description)
values
(p_tenant_id, true, 'analysis', false, true, 'Análise Pessoal', 'Minha análise pessoal');
end if;
-- -------------------------------------------------------
-- Campos padrão (idempotentes por (commitment_id, key))
-- -------------------------------------------------------
-- Leitura
select id into v_id
from public.determined_commitments
where tenant_id = p_tenant_id and is_native = true and native_key = 'reading'
limit 1;
if v_id is not null then
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'book') then
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
values (p_tenant_id, v_id, 'book', 'Livro', 'text', false, 10);
end if;
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'author') then
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
values (p_tenant_id, v_id, 'author', 'Autor', 'text', false, 20);
end if;
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'notes') then
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
values (p_tenant_id, v_id, 'notes', 'Observação', 'textarea', false, 30);
end if;
end if;
-- Supervisão
select id into v_id
from public.determined_commitments
where tenant_id = p_tenant_id and is_native = true and native_key = 'supervision'
limit 1;
if v_id is not null then
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'supervisor') then
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
values (p_tenant_id, v_id, 'supervisor', 'Supervisor', 'text', false, 10);
end if;
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'topic') then
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
values (p_tenant_id, v_id, 'topic', 'Assunto', 'text', false, 20);
end if;
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'notes') then
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
values (p_tenant_id, v_id, 'notes', 'Observação', 'textarea', false, 30);
end if;
end if;
-- Aula
select id into v_id
from public.determined_commitments
where tenant_id = p_tenant_id and is_native = true and native_key = 'class'
limit 1;
if v_id is not null then
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'theme') then
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
values (p_tenant_id, v_id, 'theme', 'Tema', 'text', false, 10);
end if;
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'group') then
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
values (p_tenant_id, v_id, 'group', 'Turma', 'text', false, 20);
end if;
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'notes') then
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
values (p_tenant_id, v_id, 'notes', 'Observação', 'textarea', false, 30);
end if;
end if;
-- Análise
select id into v_id
from public.determined_commitments
where tenant_id = p_tenant_id and is_native = true and native_key = 'analysis'
limit 1;
if v_id is not null then
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'analyst') then
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
values (p_tenant_id, v_id, 'analyst', 'Analista', 'text', false, 10);
end if;
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'focus') then
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
values (p_tenant_id, v_id, 'focus', 'Foco', 'text', false, 20);
end if;
if not exists (select 1 from public.determined_commitment_fields where commitment_id = v_id and key = 'notes') then
insert into public.determined_commitment_fields (tenant_id, commitment_id, key, label, field_type, required, sort_order)
values (p_tenant_id, v_id, 'notes', 'Observação', 'textarea', false, 30);
end if;
end if;
end;
$$;
ALTER FUNCTION public.seed_determined_commitments(p_tenant_id uuid) OWNER TO supabase_admin;
--
-- Name: set_insurance_plans_updated_at(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.set_insurance_plans_updated_at() RETURNS trigger
CREATE FUNCTION public.validate_support_session(p_token text) RETURNS json
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_session support_sessions;
BEGIN
IF p_token IS NULL OR length(trim(p_token)) < 32 THEN
RETURN json_build_object('valid', false, 'tenant_id', null);
END IF;
SELECT * INTO v_session
FROM public.support_sessions
WHERE token = p_token
AND expires_at > now()
LIMIT 1;
IF NOT FOUND THEN
RETURN json_build_object('valid', false, 'tenant_id', null);
END IF;
RETURN json_build_object(
'valid', true,
'tenant_id', v_session.tenant_id
);
END;
$$;
ALTER FUNCTION public.validate_support_session(p_token text) OWNER TO supabase_admin;
--
-- Name: whoami(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.whoami() RETURNS TABLE(uid uuid, role text)
@@ -1,404 +0,0 @@
-- =============================================================================
-- AgenciaPsi — Functions — Notificações
-- =============================================================================
-- cancel_notifications_on_opt_out, cancel_notifications_on_session_cancel,
-- cancel_patient_pending_notifications, cleanup_notification_queue,
-- notify_on_intake, notify_on_scheduling, notify_on_session_status,
-- populate_notification_queue, unstick_notification_queue
-- =============================================================================
CREATE FUNCTION public.cancel_notifications_on_opt_out() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER
AS $$
BEGIN
-- WhatsApp opt-out
IF OLD.whatsapp_opt_in = true AND NEW.whatsapp_opt_in = false THEN
PERFORM public.cancel_patient_pending_notifications(
NEW.patient_id, 'whatsapp'
);
END IF;
-- Email opt-out
IF OLD.email_opt_in = true AND NEW.email_opt_in = false THEN
PERFORM public.cancel_patient_pending_notifications(
NEW.patient_id, 'email'
);
END IF;
-- SMS opt-out
IF OLD.sms_opt_in = true AND NEW.sms_opt_in = false THEN
PERFORM public.cancel_patient_pending_notifications(
NEW.patient_id, 'sms'
);
END IF;
RETURN NEW;
END;
$$;
ALTER FUNCTION public.cancel_notifications_on_opt_out() OWNER TO supabase_admin;
--
-- Name: cancel_notifications_on_session_cancel(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.cancel_notifications_on_session_cancel() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER
AS $$
BEGIN
IF NEW.status IN ('cancelado', 'excluido')
AND OLD.status NOT IN ('cancelado', 'excluido')
THEN
PERFORM public.cancel_patient_pending_notifications(
NEW.patient_id, NULL, NEW.id
);
END IF;
RETURN NEW;
END;
$$;
ALTER FUNCTION public.cancel_notifications_on_session_cancel() OWNER TO supabase_admin;
--
-- Name: cancel_patient_pending_notifications(uuid, text, uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.cancel_patient_pending_notifications(p_patient_id uuid, p_channel text DEFAULT NULL::text, p_evento_id uuid DEFAULT NULL::uuid) RETURNS integer
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_canceled integer;
BEGIN
UPDATE public.notification_queue
SET status = 'cancelado',
updated_at = now()
WHERE patient_id = p_patient_id
AND status IN ('pendente', 'processando')
AND (p_channel IS NULL OR channel = p_channel)
AND (p_evento_id IS NULL OR agenda_evento_id = p_evento_id);
GET DIAGNOSTICS v_canceled = ROW_COUNT;
RETURN v_canceled;
END;
$$;
ALTER FUNCTION public.cancel_patient_pending_notifications(p_patient_id uuid, p_channel text, p_evento_id uuid) OWNER TO supabase_admin;
--
-- Name: cancel_recurrence_from(uuid, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.cancel_recurrence_from(p_recurrence_id uuid, p_from_date date) RETURNS void
CREATE FUNCTION public.cancel_patient_pending_notifications(p_patient_id uuid, p_channel text DEFAULT NULL::text, p_evento_id uuid DEFAULT NULL::uuid) RETURNS integer
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_canceled integer;
BEGIN
UPDATE public.notification_queue
SET status = 'cancelado',
updated_at = now()
WHERE patient_id = p_patient_id
AND status IN ('pendente', 'processando')
AND (p_channel IS NULL OR channel = p_channel)
AND (p_evento_id IS NULL OR agenda_evento_id = p_evento_id);
GET DIAGNOSTICS v_canceled = ROW_COUNT;
RETURN v_canceled;
END;
$$;
ALTER FUNCTION public.cancel_patient_pending_notifications(p_patient_id uuid, p_channel text, p_evento_id uuid) OWNER TO supabase_admin;
--
-- Name: cancel_recurrence_from(uuid, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.cancel_recurrence_from(p_recurrence_id uuid, p_from_date date) RETURNS void
CREATE FUNCTION public.cleanup_notification_queue() RETURNS integer
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_deleted integer;
BEGIN
DELETE FROM public.notification_queue
WHERE status IN ('enviado', 'cancelado', 'ignorado')
AND created_at < now() - interval '90 days';
GET DIAGNOSTICS v_deleted = ROW_COUNT;
RETURN v_deleted;
END;
$$;
ALTER FUNCTION public.cleanup_notification_queue() OWNER TO supabase_admin;
--
-- Name: create_clinic_tenant(text); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.create_clinic_tenant(p_name text) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
AS $$
declare
v_uid uuid;
v_tenant uuid;
v_name text;
begin
v_uid := auth.uid();
if v_uid is null then
raise exception 'Not authenticated';
end if;
v_name := nullif(trim(coalesce(p_name, '')), '');
if v_name is null then
v_name := 'Clínica';
end if;
insert into public.tenants (name, kind, created_at)
values (v_name, 'clinic', now())
returning id into v_tenant;
insert into public.tenant_members (tenant_id, user_id, role, status, created_at)
values (v_tenant, v_uid, 'tenant_admin', 'active', now());
return v_tenant;
end;
$$;
ALTER FUNCTION public.create_clinic_tenant(p_name text) OWNER TO supabase_admin;
--
-- Name: financial_records; Type: TABLE; Schema: public; Owner: supabase_admin
--
CREATE TABLE public.financial_records (
CREATE FUNCTION public.notify_on_intake() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER
AS $$
BEGIN
IF NEW.status = 'new' THEN
INSERT INTO public.notifications (
owner_id,
tenant_id,
type,
ref_id,
ref_table,
payload
)
VALUES (
NEW.owner_id,
NEW.tenant_id,
'new_patient',
NEW.id,
'patient_intake_requests',
jsonb_build_object(
'title', 'Novo cadastro externo',
'detail', COALESCE(NEW.nome_completo, 'Paciente'),
'deeplink', '/therapist/patients/cadastro/recebidos',
'avatar_initials', upper(left(COALESCE(NEW.nome_completo, '?'), 2))
)
);
END IF;
RETURN NEW;
END;
$$;
ALTER FUNCTION public.notify_on_intake() OWNER TO supabase_admin;
--
-- Name: notify_on_scheduling(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.notify_on_scheduling() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER
AS $$ BEGIN IF NEW.status = 'pendente' THEN
INSERT INTO public.notifications ( owner_id, tenant_id, type, ref_id, ref_table, payload ) VALUES (
NEW.owner_id, NEW.tenant_id,
'new_scheduling', NEW.id, 'agendador_solicitacoes', jsonb_build_object( 'title', 'Nova solicitação de agendamento', 'detail', COALESCE(NEW.paciente_nome, 'Paciente') || ' ' || COALESCE(NEW.paciente_sobrenome, '') || '' || COALESCE(NEW.tipo, ''), 'deeplink', '/therapist/agendamentos-recebidos', 'avatar_initials', upper(left(COALESCE(NEW.paciente_nome, '?'), 1) || left(COALESCE(NEW.paciente_sobrenome, ''), 1)) ) ); END IF; RETURN NEW; END; $$;
ALTER FUNCTION public.notify_on_scheduling() OWNER TO supabase_admin;
--
-- Name: notify_on_session_status(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.notify_on_session_status() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_nome text;
BEGIN
IF NEW.status IN ('faltou', 'cancelado') AND OLD.status IS DISTINCT FROM NEW.status THEN
SELECT nome_completo
INTO v_nome
FROM public.patients
WHERE id = NEW.patient_id
LIMIT 1;
INSERT INTO public.notifications (
owner_id,
tenant_id,
type,
ref_id,
ref_table,
payload
)
VALUES (
NEW.owner_id,
NEW.tenant_id,
'session_status',
NEW.id,
'agenda_eventos',
jsonb_build_object(
'title', CASE WHEN NEW.status = 'faltou' THEN 'Paciente faltou' ELSE 'Sessão cancelada' END,
'detail', COALESCE(v_nome, 'Paciente') || '' || to_char(NEW.inicio_em, 'DD/MM HH24:MI'),
'deeplink', '/therapist/agenda',
'avatar_initials', upper(left(COALESCE(v_nome, '?'), 2))
)
);
END IF;
RETURN NEW;
END;
$$;
ALTER FUNCTION public.notify_on_session_status() OWNER TO supabase_admin;
--
-- Name: on_new_user_seed_patient_groups(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.on_new_user_seed_patient_groups() RETURNS trigger
CREATE FUNCTION public.populate_notification_queue() RETURNS void
LANGUAGE plpgsql SECURITY DEFINER
AS $$
BEGIN
INSERT INTO public.notification_queue (
tenant_id, owner_id, agenda_evento_id, patient_id,
channel, template_key, schedule_key,
resolved_vars, recipient_address,
scheduled_at, idempotency_key
)
SELECT
ae.tenant_id,
ae.owner_id,
ae.id AS agenda_evento_id,
ae.patient_id,
ch.channel,
'session.' || REPLACE(ns.event_type, '_sessao', '') || '.' || ch.channel,
ns.schedule_key,
jsonb_build_object(
'nome_paciente', COALESCE(p.nome_completo, 'Paciente'),
'data_sessao', TO_CHAR(ae.inicio_em AT TIME ZONE 'America/Sao_Paulo', 'DD/MM/YYYY'),
'hora_sessao', TO_CHAR(ae.inicio_em AT TIME ZONE 'America/Sao_Paulo', 'HH24:MI'),
'nome_terapeuta', COALESCE(prof.full_name, 'Terapeuta'),
'modalidade', COALESCE(ae.modalidade, 'Presencial'),
'titulo', COALESCE(ae.titulo, 'Sessão')
),
CASE ch.channel
WHEN 'whatsapp' THEN COALESCE(p.telefone, '')
WHEN 'sms' THEN COALESCE(p.telefone, '')
WHEN 'email' THEN COALESCE(p.email_principal, '')
END,
CASE
WHEN (ae.inicio_em - (ns.offset_minutes || ' minutes')::interval)::time
< ns.allowed_time_start
THEN DATE_TRUNC('day', ae.inicio_em - (ns.offset_minutes || ' minutes')::interval)
+ ns.allowed_time_start
WHEN (ae.inicio_em - (ns.offset_minutes || ' minutes')::interval)::time
> ns.allowed_time_end
THEN DATE_TRUNC('day', ae.inicio_em - (ns.offset_minutes || ' minutes')::interval)
+ ns.allowed_time_start
ELSE ae.inicio_em - (ns.offset_minutes || ' minutes')::interval
END,
ae.id::text || ':' || ns.schedule_key || ':' || ch.channel || ':'
|| ae.inicio_em::date::text
FROM public.agenda_eventos ae
JOIN public.patients p ON p.id = ae.patient_id
LEFT JOIN public.profiles prof ON prof.id = ae.owner_id
JOIN public.notification_schedules ns
ON ns.owner_id = ae.owner_id
AND ns.is_active = true
AND ns.deleted_at IS NULL
AND ns.trigger_type = 'before_event'
AND ns.event_type = 'lembrete_sessao'
JOIN public.notification_channels nc
ON nc.owner_id = ae.owner_id
AND nc.is_active = true
AND nc.deleted_at IS NULL
CROSS JOIN LATERAL (
SELECT 'whatsapp' AS channel WHERE ns.whatsapp_enabled AND nc.channel = 'whatsapp'
UNION ALL
SELECT 'email' AS channel WHERE ns.email_enabled AND nc.channel = 'email'
UNION ALL
SELECT 'sms' AS channel WHERE ns.sms_enabled AND nc.channel = 'sms'
) ch
LEFT JOIN public.notification_preferences np
ON np.patient_id = ae.patient_id
AND np.owner_id = ae.owner_id
AND np.deleted_at IS NULL
WHERE
ae.inicio_em > now()
AND ae.inicio_em <= now() + interval '48 hours'
AND ae.status NOT IN ('cancelado', 'faltou')
AND CASE ch.channel
WHEN 'whatsapp' THEN COALESCE(p.telefone, '') != ''
WHEN 'sms' THEN COALESCE(p.telefone, '') != ''
WHEN 'email' THEN COALESCE(p.email_principal, '') != ''
END
AND CASE ch.channel
WHEN 'whatsapp' THEN COALESCE(np.whatsapp_opt_in, true)
WHEN 'email' THEN COALESCE(np.email_opt_in, true)
WHEN 'sms' THEN COALESCE(np.sms_opt_in, false)
END
AND EXISTS (
SELECT 1 FROM public.profiles tp
WHERE tp.id = ae.owner_id
AND COALESCE(tp.notify_reminders, true) = true
)
ON CONFLICT (idempotency_key) DO NOTHING;
END;
$$;
ALTER FUNCTION public.populate_notification_queue() OWNER TO supabase_admin;
--
-- Name: prevent_promoting_to_system(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.prevent_promoting_to_system() RETURNS trigger
CREATE FUNCTION public.unstick_notification_queue() RETURNS integer
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_unstuck integer;
BEGIN
UPDATE public.notification_queue
SET status = 'pendente',
attempts = attempts + 1,
last_error = 'Timeout: preso em processando por >10min',
next_retry_at = now() + interval '2 minutes'
WHERE status = 'processando'
AND updated_at < now() - interval '10 minutes';
GET DIAGNOSTICS v_unstuck = ROW_COUNT;
RETURN v_unstuck;
END;
$$;
ALTER FUNCTION public.unstick_notification_queue() OWNER TO supabase_admin;
--
-- Name: update_payment_settings_updated_at(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.update_payment_settings_updated_at() RETURNS trigger
@@ -1,433 +0,0 @@
-- =============================================================================
-- 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
@@ -0,0 +1,22 @@
-- Functions: pgbouncer
-- Gerado automaticamente em 2026-04-17T12:23:05.222Z
-- Total: 1
CREATE FUNCTION pgbouncer.get_auth(p_usename text) RETURNS TABLE(username text, password text)
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO ''
AS $_$
begin
raise debug 'PgBouncer auth request: %', p_usename;
return query
select
rolname::text,
case when rolvaliduntil < now()
then null
else rolpassword::text
end
from pg_authid
where rolname=$1 and rolcanlogin;
end;
$_$;
File diff suppressed because it is too large Load Diff
+3 -88
View File
@@ -1,6 +1,6 @@
-- =============================================================================
-- AgenciaPsi — Functions — realtime schema
-- =============================================================================
-- Functions: realtime
-- Gerado automaticamente em 2026-04-17T12:23:05.223Z
-- Total: 12
CREATE FUNCTION realtime.apply_rls(wal jsonb, max_record_bytes integer DEFAULT (1024 * 1024)) RETURNS SETOF realtime.wal_rls
LANGUAGE plpgsql
@@ -302,13 +302,6 @@ perform set_config('role', null, true);
end;
$$;
ALTER FUNCTION realtime.apply_rls(wal jsonb, max_record_bytes integer) OWNER TO supabase_admin;
--
-- Name: broadcast_changes(text, text, text, text, text, record, record, text); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
--
CREATE FUNCTION realtime.broadcast_changes(topic_name text, event_name text, operation text, table_name text, table_schema text, new record, old record, level text DEFAULT 'ROW'::text) RETURNS void
LANGUAGE plpgsql
AS $$
@@ -333,13 +326,6 @@ END;
$$;
ALTER FUNCTION realtime.broadcast_changes(topic_name text, event_name text, operation text, table_name text, table_schema text, new record, old record, level text) OWNER TO supabase_admin;
--
-- Name: build_prepared_statement_sql(text, regclass, realtime.wal_column[]); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
--
CREATE FUNCTION realtime.build_prepared_statement_sql(prepared_statement_name text, entity regclass, columns realtime.wal_column[]) RETURNS text
LANGUAGE sql
AS $$
@@ -368,13 +354,6 @@ CREATE FUNCTION realtime.build_prepared_statement_sql(prepared_statement_name te
entity
$$;
ALTER FUNCTION realtime.build_prepared_statement_sql(prepared_statement_name text, entity regclass, columns realtime.wal_column[]) OWNER TO supabase_admin;
--
-- Name: cast(text, regtype); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
--
CREATE FUNCTION realtime."cast"(val text, type_ regtype) RETURNS jsonb
LANGUAGE plpgsql IMMUTABLE
AS $$
@@ -386,13 +365,6 @@ CREATE FUNCTION realtime."cast"(val text, type_ regtype) RETURNS jsonb
end
$$;
ALTER FUNCTION realtime."cast"(val text, type_ regtype) OWNER TO supabase_admin;
--
-- Name: check_equality_op(realtime.equality_op, regtype, text, text); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
--
CREATE FUNCTION realtime.check_equality_op(op realtime.equality_op, type_ regtype, val_1 text, val_2 text) RETURNS boolean
LANGUAGE plpgsql IMMUTABLE
AS $$
@@ -427,13 +399,6 @@ CREATE FUNCTION realtime.check_equality_op(op realtime.equality_op, type_ regtyp
end;
$$;
ALTER FUNCTION realtime.check_equality_op(op realtime.equality_op, type_ regtype, val_1 text, val_2 text) OWNER TO supabase_admin;
--
-- Name: is_visible_through_filters(realtime.wal_column[], realtime.user_defined_filter[]); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
--
CREATE FUNCTION realtime.is_visible_through_filters(columns realtime.wal_column[], filters realtime.user_defined_filter[]) RETURNS boolean
LANGUAGE sql IMMUTABLE
AS $_$
@@ -465,13 +430,6 @@ CREATE FUNCTION realtime.is_visible_through_filters(columns realtime.wal_column[
on f.column_name = col.name;
$_$;
ALTER FUNCTION realtime.is_visible_through_filters(columns realtime.wal_column[], filters realtime.user_defined_filter[]) OWNER TO supabase_admin;
--
-- Name: list_changes(name, name, integer, integer); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
--
CREATE FUNCTION realtime.list_changes(publication name, slot_name name, max_changes integer, max_record_bytes integer) RETURNS SETOF realtime.wal_rls
LANGUAGE sql
SET log_min_messages TO 'fatal'
@@ -533,13 +491,6 @@ CREATE FUNCTION realtime.list_changes(publication name, slot_name name, max_chan
and xyz.subscription_ids[1] is not null
$$;
ALTER FUNCTION realtime.list_changes(publication name, slot_name name, max_changes integer, max_record_bytes integer) OWNER TO supabase_admin;
--
-- Name: quote_wal2json(regclass); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
--
CREATE FUNCTION realtime.quote_wal2json(entity regclass) RETURNS text
LANGUAGE sql IMMUTABLE STRICT
AS $$
@@ -573,13 +524,6 @@ CREATE FUNCTION realtime.quote_wal2json(entity regclass) RETURNS text
pc.oid = entity
$$;
ALTER FUNCTION realtime.quote_wal2json(entity regclass) OWNER TO supabase_admin;
--
-- Name: send(jsonb, text, text, boolean); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
--
CREATE FUNCTION realtime.send(payload jsonb, event text, topic text, private boolean DEFAULT true) RETURNS void
LANGUAGE plpgsql
AS $$
@@ -612,13 +556,6 @@ BEGIN
END;
$$;
ALTER FUNCTION realtime.send(payload jsonb, event text, topic text, private boolean) OWNER TO supabase_admin;
--
-- Name: subscription_check_filters(); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
--
CREATE FUNCTION realtime.subscription_check_filters() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -688,34 +625,12 @@ CREATE FUNCTION realtime.subscription_check_filters() RETURNS trigger
end;
$$;
ALTER FUNCTION realtime.subscription_check_filters() OWNER TO supabase_admin;
--
-- Name: to_regrole(text); Type: FUNCTION; Schema: realtime; Owner: supabase_admin
--
CREATE FUNCTION realtime.to_regrole(role_name text) RETURNS regrole
LANGUAGE sql IMMUTABLE
AS $$ select role_name::regrole $$;
ALTER FUNCTION realtime.to_regrole(role_name text) OWNER TO supabase_admin;
--
-- Name: topic(); Type: FUNCTION; Schema: realtime; Owner: supabase_realtime_admin
--
CREATE FUNCTION realtime.topic() RETURNS text
LANGUAGE sql STABLE
AS $$
select nullif(current_setting('realtime.topic', true), '')::text;
$$;
ALTER FUNCTION realtime.topic() OWNER TO supabase_realtime_admin;
--
-- Name: can_insert_object(text, text, uuid, jsonb); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
+3 -109
View File
@@ -1,6 +1,6 @@
-- =============================================================================
-- AgenciaPsi — Functions — storage schema
-- =============================================================================
-- Functions: storage
-- Gerado automaticamente em 2026-04-17T12:23:05.224Z
-- Total: 15
CREATE FUNCTION storage.can_insert_object(bucketid text, name text, owner uuid, metadata jsonb) RETURNS void
LANGUAGE plpgsql
@@ -14,13 +14,6 @@ BEGIN
END
$$;
ALTER FUNCTION storage.can_insert_object(bucketid text, name text, owner uuid, metadata jsonb) OWNER TO supabase_storage_admin;
--
-- Name: enforce_bucket_name_length(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
CREATE FUNCTION storage.enforce_bucket_name_length() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -32,13 +25,6 @@ begin
end;
$$;
ALTER FUNCTION storage.enforce_bucket_name_length() OWNER TO supabase_storage_admin;
--
-- Name: extension(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
CREATE FUNCTION storage.extension(name text) RETURNS text
LANGUAGE plpgsql
AS $$
@@ -53,13 +39,6 @@ BEGIN
END
$$;
ALTER FUNCTION storage.extension(name text) OWNER TO supabase_storage_admin;
--
-- Name: filename(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
CREATE FUNCTION storage.filename(name text) RETURNS text
LANGUAGE plpgsql
AS $$
@@ -71,13 +50,6 @@ BEGIN
END
$$;
ALTER FUNCTION storage.filename(name text) OWNER TO supabase_storage_admin;
--
-- Name: foldername(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
CREATE FUNCTION storage.foldername(name text) RETURNS text[]
LANGUAGE plpgsql
AS $$
@@ -89,13 +61,6 @@ BEGIN
END
$$;
ALTER FUNCTION storage.foldername(name text) OWNER TO supabase_storage_admin;
--
-- Name: get_common_prefix(text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
CREATE FUNCTION storage.get_common_prefix(p_key text, p_prefix text, p_delimiter text) RETURNS text
LANGUAGE sql IMMUTABLE
AS $$
@@ -106,13 +71,6 @@ SELECT CASE
END;
$$;
ALTER FUNCTION storage.get_common_prefix(p_key text, p_prefix text, p_delimiter text) OWNER TO supabase_storage_admin;
--
-- Name: get_size_by_bucket(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
CREATE FUNCTION storage.get_size_by_bucket() RETURNS TABLE(size bigint, bucket_id text)
LANGUAGE plpgsql
AS $$
@@ -124,13 +82,6 @@ BEGIN
END
$$;
ALTER FUNCTION storage.get_size_by_bucket() OWNER TO supabase_storage_admin;
--
-- Name: list_multipart_uploads_with_delimiter(text, text, text, integer, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
CREATE FUNCTION storage.list_multipart_uploads_with_delimiter(bucket_id text, prefix_param text, delimiter_param text, max_keys integer DEFAULT 100, next_key_token text DEFAULT ''::text, next_upload_token text DEFAULT ''::text) RETURNS TABLE(key text, id text, created_at timestamp with time zone)
LANGUAGE plpgsql
AS $_$
@@ -172,13 +123,6 @@ BEGIN
END;
$_$;
ALTER FUNCTION storage.list_multipart_uploads_with_delimiter(bucket_id text, prefix_param text, delimiter_param text, max_keys integer, next_key_token text, next_upload_token text) OWNER TO supabase_storage_admin;
--
-- Name: list_objects_with_delimiter(text, text, text, integer, text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
CREATE FUNCTION storage.list_objects_with_delimiter(_bucket_id text, prefix_param text, delimiter_param text, max_keys integer DEFAULT 100, start_after text DEFAULT ''::text, next_token text DEFAULT ''::text, sort_order text DEFAULT 'asc'::text) RETURNS TABLE(name text, id uuid, metadata jsonb, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone)
LANGUAGE plpgsql STABLE
AS $_$
@@ -389,13 +333,6 @@ BEGIN
END;
$_$;
ALTER FUNCTION storage.list_objects_with_delimiter(_bucket_id text, prefix_param text, delimiter_param text, max_keys integer, start_after text, next_token text, sort_order text) OWNER TO supabase_storage_admin;
--
-- Name: operation(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
CREATE FUNCTION storage.operation() RETURNS text
LANGUAGE plpgsql STABLE
AS $$
@@ -404,13 +341,6 @@ BEGIN
END;
$$;
ALTER FUNCTION storage.operation() OWNER TO supabase_storage_admin;
--
-- Name: protect_delete(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
CREATE FUNCTION storage.protect_delete() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -425,13 +355,6 @@ BEGIN
END;
$$;
ALTER FUNCTION storage.protect_delete() OWNER TO supabase_storage_admin;
--
-- Name: search(text, text, integer, integer, integer, text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
CREATE FUNCTION storage.search(prefix text, bucketname text, limits integer DEFAULT 100, levels integer DEFAULT 1, offsets integer DEFAULT 0, search text DEFAULT ''::text, sortcolumn text DEFAULT 'name'::text, sortorder text DEFAULT 'asc'::text) RETURNS TABLE(name text, id uuid, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone, metadata jsonb)
LANGUAGE plpgsql STABLE
AS $_$
@@ -681,13 +604,6 @@ BEGIN
END;
$_$;
ALTER FUNCTION storage.search(prefix text, bucketname text, limits integer, levels integer, offsets integer, search text, sortcolumn text, sortorder text) OWNER TO supabase_storage_admin;
--
-- Name: search_by_timestamp(text, text, integer, integer, text, text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
CREATE FUNCTION storage.search_by_timestamp(p_prefix text, p_bucket_id text, p_limit integer, p_level integer, p_start_after text, p_sort_order text, p_sort_column text, p_sort_column_after text) RETURNS TABLE(key text, name text, id uuid, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone, metadata jsonb)
LANGUAGE plpgsql STABLE
AS $_$
@@ -790,13 +706,6 @@ BEGIN
END;
$_$;
ALTER FUNCTION storage.search_by_timestamp(p_prefix text, p_bucket_id text, p_limit integer, p_level integer, p_start_after text, p_sort_order text, p_sort_column text, p_sort_column_after text) OWNER TO supabase_storage_admin;
--
-- Name: search_v2(text, text, integer, integer, text, text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
CREATE FUNCTION storage.search_v2(prefix text, bucket_name text, limits integer DEFAULT 100, levels integer DEFAULT 1, start_after text DEFAULT ''::text, sort_order text DEFAULT 'asc'::text, sort_column text DEFAULT 'name'::text, sort_column_after text DEFAULT ''::text) RETURNS TABLE(key text, name text, id uuid, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone, metadata jsonb)
LANGUAGE plpgsql STABLE
AS $$
@@ -852,13 +761,6 @@ BEGIN
END;
$$;
ALTER FUNCTION storage.search_v2(prefix text, bucket_name text, limits integer, levels integer, start_after text, sort_order text, sort_column text, sort_column_after text) OWNER TO supabase_storage_admin;
--
-- Name: update_updated_at_column(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
--
CREATE FUNCTION storage.update_updated_at_column() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -867,11 +769,3 @@ BEGIN
RETURN NEW;
END;
$$;
ALTER FUNCTION storage.update_updated_at_column() OWNER TO supabase_storage_admin;
--
-- Name: http_request(); Type: FUNCTION; Schema: supabase_functions; Owner: supabase_functions_admin
--
@@ -1,6 +1,6 @@
-- =============================================================================
-- AgenciaPsi — Functions — supabase_functions schema
-- =============================================================================
-- Functions: supabase_functions
-- Gerado automaticamente em 2026-04-17T12:23:05.224Z
-- Total: 1
CREATE FUNCTION supabase_functions.http_request() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER
@@ -77,11 +77,3 @@ CREATE FUNCTION supabase_functions.http_request() RETURNS trigger
RETURN NEW;
END
$$;
ALTER FUNCTION supabase_functions.http_request() OWNER TO supabase_functions_admin;
--
-- Name: extensions; Type: TABLE; Schema: _realtime; Owner: supabase_admin
--