Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras
This commit is contained in:
650
database-novo/schema/03_functions/agenda.sql
Normal file
650
database-novo/schema/03_functions/agenda.sql
Normal file
@@ -0,0 +1,650 @@
|
||||
-- =============================================================================
|
||||
-- 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
|
||||
93
database-novo/schema/03_functions/auth.sql
Normal file
93
database-novo/schema/03_functions/auth.sql
Normal file
@@ -0,0 +1,93 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Functions — auth schema
|
||||
-- auth.email(), auth.jwt(), auth.role(), auth.uid()
|
||||
-- =============================================================================
|
||||
|
||||
--
|
||||
-- Name: email(); Type: FUNCTION; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE FUNCTION auth.email() RETURNS text
|
||||
LANGUAGE sql STABLE
|
||||
AS $$
|
||||
select
|
||||
coalesce(
|
||||
nullif(current_setting('request.jwt.claim.email', true), ''),
|
||||
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'email')
|
||||
)::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 $$
|
||||
select
|
||||
coalesce(
|
||||
nullif(current_setting('request.jwt.claim', true), ''),
|
||||
nullif(current_setting('request.jwt.claims', true), '')
|
||||
)::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 $$
|
||||
select
|
||||
coalesce(
|
||||
nullif(current_setting('request.jwt.claim.role', true), ''),
|
||||
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role')
|
||||
)::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 $$
|
||||
select
|
||||
coalesce(
|
||||
nullif(current_setting('request.jwt.claim.sub', true), ''),
|
||||
(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.';
|
||||
|
||||
1283
database-novo/schema/03_functions/billing.sql
Normal file
1283
database-novo/schema/03_functions/billing.sql
Normal file
File diff suppressed because it is too large
Load Diff
2350
database-novo/schema/03_functions/core.sql
Normal file
2350
database-novo/schema/03_functions/core.sql
Normal file
File diff suppressed because it is too large
Load Diff
818
database-novo/schema/03_functions/financial.sql
Normal file
818
database-novo/schema/03_functions/financial.sql
Normal file
@@ -0,0 +1,818 @@
|
||||
-- =============================================================================
|
||||
-- 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
|
||||
316
database-novo/schema/03_functions/infra.sql
Normal file
316
database-novo/schema/03_functions/infra.sql
Normal file
@@ -0,0 +1,316 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Functions — infraestrutura
|
||||
-- extensions.grant_pg_*, pgbouncer.get_auth, etc.
|
||||
-- =============================================================================
|
||||
|
||||
CREATE FUNCTION extensions.grant_pg_cron_access() RETURNS event_trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT
|
||||
FROM pg_event_trigger_ddl_commands() AS ev
|
||||
JOIN pg_extension AS ext
|
||||
ON ev.objid = ext.oid
|
||||
WHERE ext.extname = 'pg_cron'
|
||||
)
|
||||
THEN
|
||||
grant usage on schema cron to postgres with grant option;
|
||||
|
||||
alter default privileges in schema cron grant all on tables to postgres with grant option;
|
||||
alter default privileges in schema cron grant all on functions to postgres with grant option;
|
||||
alter default privileges in schema cron grant all on sequences to postgres with grant option;
|
||||
|
||||
alter default privileges for user supabase_admin in schema cron grant all
|
||||
on sequences to postgres with grant option;
|
||||
alter default privileges for user supabase_admin in schema cron grant all
|
||||
on tables to postgres with grant option;
|
||||
alter default privileges for user supabase_admin in schema cron grant all
|
||||
on functions to postgres with grant option;
|
||||
|
||||
grant all privileges on all tables in schema cron to postgres with grant option;
|
||||
revoke all on table cron.job from postgres;
|
||||
grant select on table cron.job to postgres with grant option;
|
||||
END IF;
|
||||
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 $_$
|
||||
DECLARE
|
||||
func_is_graphql_resolve bool;
|
||||
BEGIN
|
||||
func_is_graphql_resolve = (
|
||||
SELECT n.proname = 'resolve'
|
||||
FROM pg_event_trigger_ddl_commands() AS ev
|
||||
LEFT JOIN pg_catalog.pg_proc AS n
|
||||
ON ev.objid = n.oid
|
||||
);
|
||||
|
||||
IF func_is_graphql_resolve
|
||||
THEN
|
||||
-- Update public wrapper to pass all arguments through to the pg_graphql resolve func
|
||||
DROP FUNCTION IF EXISTS graphql_public.graphql;
|
||||
create or replace function graphql_public.graphql(
|
||||
"operationName" text default null,
|
||||
query text default null,
|
||||
variables jsonb default null,
|
||||
extensions jsonb default null
|
||||
)
|
||||
returns jsonb
|
||||
language sql
|
||||
as $$
|
||||
select graphql.resolve(
|
||||
query := query,
|
||||
variables := coalesce(variables, '{}'),
|
||||
"operationName" := "operationName",
|
||||
extensions := extensions
|
||||
);
|
||||
$$;
|
||||
|
||||
-- This hook executes when `graphql.resolve` is created. That is not necessarily the last
|
||||
-- function in the extension so we need to grant permissions on existing entities AND
|
||||
-- update default permissions to any others that are created after `graphql.resolve`
|
||||
grant usage on schema graphql to postgres, anon, authenticated, service_role;
|
||||
grant select on all tables in schema graphql to postgres, anon, authenticated, service_role;
|
||||
grant execute on all functions in schema graphql to postgres, anon, authenticated, service_role;
|
||||
grant all on all sequences in schema graphql to postgres, anon, authenticated, service_role;
|
||||
alter default privileges in schema graphql grant all on tables to postgres, anon, authenticated, service_role;
|
||||
alter default privileges in schema graphql grant all on functions to postgres, anon, authenticated, service_role;
|
||||
alter default privileges in schema graphql grant all on sequences to postgres, anon, authenticated, service_role;
|
||||
|
||||
-- Allow postgres role to allow granting usage on graphql and graphql_public schemas to custom roles
|
||||
grant usage on schema graphql_public to postgres with grant option;
|
||||
grant usage on schema graphql to postgres with grant option;
|
||||
END IF;
|
||||
|
||||
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 $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_event_trigger_ddl_commands() AS ev
|
||||
JOIN pg_extension AS ext
|
||||
ON ev.objid = ext.oid
|
||||
WHERE ext.extname = 'pg_net'
|
||||
)
|
||||
THEN
|
||||
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
|
||||
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
END IF;
|
||||
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 $$
|
||||
DECLARE
|
||||
cmd record;
|
||||
BEGIN
|
||||
FOR cmd IN SELECT * FROM pg_event_trigger_ddl_commands()
|
||||
LOOP
|
||||
IF cmd.command_tag IN (
|
||||
'CREATE SCHEMA', 'ALTER SCHEMA'
|
||||
, 'CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO', 'ALTER TABLE'
|
||||
, 'CREATE FOREIGN TABLE', 'ALTER FOREIGN TABLE'
|
||||
, 'CREATE VIEW', 'ALTER VIEW'
|
||||
, 'CREATE MATERIALIZED VIEW', 'ALTER MATERIALIZED VIEW'
|
||||
, 'CREATE FUNCTION', 'ALTER FUNCTION'
|
||||
, 'CREATE TRIGGER'
|
||||
, 'CREATE TYPE', 'ALTER TYPE'
|
||||
, 'CREATE RULE'
|
||||
, 'COMMENT'
|
||||
)
|
||||
-- don't notify in case of CREATE TEMP table or other objects created on pg_temp
|
||||
AND cmd.schema_name is distinct from 'pg_temp'
|
||||
THEN
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
END IF;
|
||||
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 $$
|
||||
DECLARE
|
||||
obj record;
|
||||
BEGIN
|
||||
FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects()
|
||||
LOOP
|
||||
IF obj.object_type IN (
|
||||
'schema'
|
||||
, 'table'
|
||||
, 'foreign table'
|
||||
, 'view'
|
||||
, 'materialized view'
|
||||
, 'function'
|
||||
, 'trigger'
|
||||
, 'type'
|
||||
, 'rule'
|
||||
)
|
||||
AND obj.is_temporary IS false -- no pg_temp objects
|
||||
THEN
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
END IF;
|
||||
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 $_$
|
||||
DECLARE
|
||||
graphql_is_dropped bool;
|
||||
BEGIN
|
||||
graphql_is_dropped = (
|
||||
SELECT ev.schema_name = 'graphql_public'
|
||||
FROM pg_event_trigger_dropped_objects() AS ev
|
||||
WHERE ev.schema_name = 'graphql_public'
|
||||
);
|
||||
|
||||
IF graphql_is_dropped
|
||||
THEN
|
||||
create or replace function graphql_public.graphql(
|
||||
"operationName" text default null,
|
||||
query text default null,
|
||||
variables jsonb default null,
|
||||
extensions jsonb default null
|
||||
)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
as $$
|
||||
DECLARE
|
||||
server_version float;
|
||||
BEGIN
|
||||
server_version = (SELECT (SPLIT_PART((select version()), ' ', 2))::float);
|
||||
|
||||
IF server_version >= 14 THEN
|
||||
RETURN jsonb_build_object(
|
||||
'errors', jsonb_build_array(
|
||||
jsonb_build_object(
|
||||
'message', 'pg_graphql extension is not enabled.'
|
||||
)
|
||||
)
|
||||
);
|
||||
ELSE
|
||||
RETURN jsonb_build_object(
|
||||
'errors', jsonb_build_array(
|
||||
jsonb_build_object(
|
||||
'message', 'pg_graphql is only available on projects running Postgres 14 onwards.'
|
||||
)
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
END IF;
|
||||
|
||||
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;
|
||||
776
database-novo/schema/03_functions/misc.sql
Normal file
776
database-novo/schema/03_functions/misc.sql
Normal file
@@ -0,0 +1,776 @@
|
||||
-- =============================================================================
|
||||
-- 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)
|
||||
404
database-novo/schema/03_functions/notifications.sql
Normal file
404
database-novo/schema/03_functions/notifications.sql
Normal file
@@ -0,0 +1,404 @@
|
||||
-- =============================================================================
|
||||
-- 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
|
||||
433
database-novo/schema/03_functions/patients.sql
Normal file
433
database-novo/schema/03_functions/patients.sql
Normal file
@@ -0,0 +1,433 @@
|
||||
-- =============================================================================
|
||||
-- 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
|
||||
721
database-novo/schema/03_functions/realtime.sql
Normal file
721
database-novo/schema/03_functions/realtime.sql
Normal file
@@ -0,0 +1,721 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Functions — realtime schema
|
||||
-- =============================================================================
|
||||
|
||||
CREATE FUNCTION realtime.apply_rls(wal jsonb, max_record_bytes integer DEFAULT (1024 * 1024)) RETURNS SETOF realtime.wal_rls
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
declare
|
||||
-- Regclass of the table e.g. public.notes
|
||||
entity_ regclass = (quote_ident(wal ->> 'schema') || '.' || quote_ident(wal ->> 'table'))::regclass;
|
||||
|
||||
-- I, U, D, T: insert, update ...
|
||||
action realtime.action = (
|
||||
case wal ->> 'action'
|
||||
when 'I' then 'INSERT'
|
||||
when 'U' then 'UPDATE'
|
||||
when 'D' then 'DELETE'
|
||||
else 'ERROR'
|
||||
end
|
||||
);
|
||||
|
||||
-- Is row level security enabled for the table
|
||||
is_rls_enabled bool = relrowsecurity from pg_class where oid = entity_;
|
||||
|
||||
subscriptions realtime.subscription[] = array_agg(subs)
|
||||
from
|
||||
realtime.subscription subs
|
||||
where
|
||||
subs.entity = entity_;
|
||||
|
||||
-- Subscription vars
|
||||
roles regrole[] = array_agg(distinct us.claims_role::text)
|
||||
from
|
||||
unnest(subscriptions) us;
|
||||
|
||||
working_role regrole;
|
||||
claimed_role regrole;
|
||||
claims jsonb;
|
||||
|
||||
subscription_id uuid;
|
||||
subscription_has_access bool;
|
||||
visible_to_subscription_ids uuid[] = '{}';
|
||||
|
||||
-- structured info for wal's columns
|
||||
columns realtime.wal_column[];
|
||||
-- previous identity values for update/delete
|
||||
old_columns realtime.wal_column[];
|
||||
|
||||
error_record_exceeds_max_size boolean = octet_length(wal::text) > max_record_bytes;
|
||||
|
||||
-- Primary jsonb output for record
|
||||
output jsonb;
|
||||
|
||||
begin
|
||||
perform set_config('role', null, true);
|
||||
|
||||
columns =
|
||||
array_agg(
|
||||
(
|
||||
x->>'name',
|
||||
x->>'type',
|
||||
x->>'typeoid',
|
||||
realtime.cast(
|
||||
(x->'value') #>> '{}',
|
||||
coalesce(
|
||||
(x->>'typeoid')::regtype, -- null when wal2json version <= 2.4
|
||||
(x->>'type')::regtype
|
||||
)
|
||||
),
|
||||
(pks ->> 'name') is not null,
|
||||
true
|
||||
)::realtime.wal_column
|
||||
)
|
||||
from
|
||||
jsonb_array_elements(wal -> 'columns') x
|
||||
left join jsonb_array_elements(wal -> 'pk') pks
|
||||
on (x ->> 'name') = (pks ->> 'name');
|
||||
|
||||
old_columns =
|
||||
array_agg(
|
||||
(
|
||||
x->>'name',
|
||||
x->>'type',
|
||||
x->>'typeoid',
|
||||
realtime.cast(
|
||||
(x->'value') #>> '{}',
|
||||
coalesce(
|
||||
(x->>'typeoid')::regtype, -- null when wal2json version <= 2.4
|
||||
(x->>'type')::regtype
|
||||
)
|
||||
),
|
||||
(pks ->> 'name') is not null,
|
||||
true
|
||||
)::realtime.wal_column
|
||||
)
|
||||
from
|
||||
jsonb_array_elements(wal -> 'identity') x
|
||||
left join jsonb_array_elements(wal -> 'pk') pks
|
||||
on (x ->> 'name') = (pks ->> 'name');
|
||||
|
||||
for working_role in select * from unnest(roles) loop
|
||||
|
||||
-- Update `is_selectable` for columns and old_columns
|
||||
columns =
|
||||
array_agg(
|
||||
(
|
||||
c.name,
|
||||
c.type_name,
|
||||
c.type_oid,
|
||||
c.value,
|
||||
c.is_pkey,
|
||||
pg_catalog.has_column_privilege(working_role, entity_, c.name, 'SELECT')
|
||||
)::realtime.wal_column
|
||||
)
|
||||
from
|
||||
unnest(columns) c;
|
||||
|
||||
old_columns =
|
||||
array_agg(
|
||||
(
|
||||
c.name,
|
||||
c.type_name,
|
||||
c.type_oid,
|
||||
c.value,
|
||||
c.is_pkey,
|
||||
pg_catalog.has_column_privilege(working_role, entity_, c.name, 'SELECT')
|
||||
)::realtime.wal_column
|
||||
)
|
||||
from
|
||||
unnest(old_columns) c;
|
||||
|
||||
if action <> 'DELETE' and count(1) = 0 from unnest(columns) c where c.is_pkey then
|
||||
return next (
|
||||
jsonb_build_object(
|
||||
'schema', wal ->> 'schema',
|
||||
'table', wal ->> 'table',
|
||||
'type', action
|
||||
),
|
||||
is_rls_enabled,
|
||||
-- subscriptions is already filtered by entity
|
||||
(select array_agg(s.subscription_id) from unnest(subscriptions) as s where claims_role = working_role),
|
||||
array['Error 400: Bad Request, no primary key']
|
||||
)::realtime.wal_rls;
|
||||
|
||||
-- The claims role does not have SELECT permission to the primary key of entity
|
||||
elsif action <> 'DELETE' and sum(c.is_selectable::int) <> count(1) from unnest(columns) c where c.is_pkey then
|
||||
return next (
|
||||
jsonb_build_object(
|
||||
'schema', wal ->> 'schema',
|
||||
'table', wal ->> 'table',
|
||||
'type', action
|
||||
),
|
||||
is_rls_enabled,
|
||||
(select array_agg(s.subscription_id) from unnest(subscriptions) as s where claims_role = working_role),
|
||||
array['Error 401: Unauthorized']
|
||||
)::realtime.wal_rls;
|
||||
|
||||
else
|
||||
output = jsonb_build_object(
|
||||
'schema', wal ->> 'schema',
|
||||
'table', wal ->> 'table',
|
||||
'type', action,
|
||||
'commit_timestamp', to_char(
|
||||
((wal ->> 'timestamp')::timestamptz at time zone 'utc'),
|
||||
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
||||
),
|
||||
'columns', (
|
||||
select
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'name', pa.attname,
|
||||
'type', pt.typname
|
||||
)
|
||||
order by pa.attnum asc
|
||||
)
|
||||
from
|
||||
pg_attribute pa
|
||||
join pg_type pt
|
||||
on pa.atttypid = pt.oid
|
||||
where
|
||||
attrelid = entity_
|
||||
and attnum > 0
|
||||
and pg_catalog.has_column_privilege(working_role, entity_, pa.attname, 'SELECT')
|
||||
)
|
||||
)
|
||||
-- Add "record" key for insert and update
|
||||
|| case
|
||||
when action in ('INSERT', 'UPDATE') then
|
||||
jsonb_build_object(
|
||||
'record',
|
||||
(
|
||||
select
|
||||
jsonb_object_agg(
|
||||
-- if unchanged toast, get column name and value from old record
|
||||
coalesce((c).name, (oc).name),
|
||||
case
|
||||
when (c).name is null then (oc).value
|
||||
else (c).value
|
||||
end
|
||||
)
|
||||
from
|
||||
unnest(columns) c
|
||||
full outer join unnest(old_columns) oc
|
||||
on (c).name = (oc).name
|
||||
where
|
||||
coalesce((c).is_selectable, (oc).is_selectable)
|
||||
and ( not error_record_exceeds_max_size or (octet_length((c).value::text) <= 64))
|
||||
)
|
||||
)
|
||||
else '{}'::jsonb
|
||||
end
|
||||
-- Add "old_record" key for update and delete
|
||||
|| case
|
||||
when action = 'UPDATE' then
|
||||
jsonb_build_object(
|
||||
'old_record',
|
||||
(
|
||||
select jsonb_object_agg((c).name, (c).value)
|
||||
from unnest(old_columns) c
|
||||
where
|
||||
(c).is_selectable
|
||||
and ( not error_record_exceeds_max_size or (octet_length((c).value::text) <= 64))
|
||||
)
|
||||
)
|
||||
when action = 'DELETE' then
|
||||
jsonb_build_object(
|
||||
'old_record',
|
||||
(
|
||||
select jsonb_object_agg((c).name, (c).value)
|
||||
from unnest(old_columns) c
|
||||
where
|
||||
(c).is_selectable
|
||||
and ( not error_record_exceeds_max_size or (octet_length((c).value::text) <= 64))
|
||||
and ( not is_rls_enabled or (c).is_pkey ) -- if RLS enabled, we can't secure deletes so filter to pkey
|
||||
)
|
||||
)
|
||||
else '{}'::jsonb
|
||||
end;
|
||||
|
||||
-- Create the prepared statement
|
||||
if is_rls_enabled and action <> 'DELETE' then
|
||||
if (select 1 from pg_prepared_statements where name = 'walrus_rls_stmt' limit 1) > 0 then
|
||||
deallocate walrus_rls_stmt;
|
||||
end if;
|
||||
execute realtime.build_prepared_statement_sql('walrus_rls_stmt', entity_, columns);
|
||||
end if;
|
||||
|
||||
visible_to_subscription_ids = '{}';
|
||||
|
||||
for subscription_id, claims in (
|
||||
select
|
||||
subs.subscription_id,
|
||||
subs.claims
|
||||
from
|
||||
unnest(subscriptions) subs
|
||||
where
|
||||
subs.entity = entity_
|
||||
and subs.claims_role = working_role
|
||||
and (
|
||||
realtime.is_visible_through_filters(columns, subs.filters)
|
||||
or (
|
||||
action = 'DELETE'
|
||||
and realtime.is_visible_through_filters(old_columns, subs.filters)
|
||||
)
|
||||
)
|
||||
) loop
|
||||
|
||||
if not is_rls_enabled or action = 'DELETE' then
|
||||
visible_to_subscription_ids = visible_to_subscription_ids || subscription_id;
|
||||
else
|
||||
-- Check if RLS allows the role to see the record
|
||||
perform
|
||||
-- Trim leading and trailing quotes from working_role because set_config
|
||||
-- doesn't recognize the role as valid if they are included
|
||||
set_config('role', trim(both '"' from working_role::text), true),
|
||||
set_config('request.jwt.claims', claims::text, true);
|
||||
|
||||
execute 'execute walrus_rls_stmt' into subscription_has_access;
|
||||
|
||||
if subscription_has_access then
|
||||
visible_to_subscription_ids = visible_to_subscription_ids || subscription_id;
|
||||
end if;
|
||||
end if;
|
||||
end loop;
|
||||
|
||||
perform set_config('role', null, true);
|
||||
|
||||
return next (
|
||||
output,
|
||||
is_rls_enabled,
|
||||
visible_to_subscription_ids,
|
||||
case
|
||||
when error_record_exceeds_max_size then array['Error 413: Payload Too Large']
|
||||
else '{}'
|
||||
end
|
||||
)::realtime.wal_rls;
|
||||
|
||||
end if;
|
||||
end loop;
|
||||
|
||||
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 $$
|
||||
DECLARE
|
||||
-- Declare a variable to hold the JSONB representation of the row
|
||||
row_data jsonb := '{}'::jsonb;
|
||||
BEGIN
|
||||
IF level = 'STATEMENT' THEN
|
||||
RAISE EXCEPTION 'function can only be triggered for each row, not for each statement';
|
||||
END IF;
|
||||
-- Check the operation type and handle accordingly
|
||||
IF operation = 'INSERT' OR operation = 'UPDATE' OR operation = 'DELETE' THEN
|
||||
row_data := jsonb_build_object('old_record', OLD, 'record', NEW, 'operation', operation, 'table', table_name, 'schema', table_schema);
|
||||
PERFORM realtime.send (row_data, event_name, topic_name);
|
||||
ELSE
|
||||
RAISE EXCEPTION 'Unexpected operation type: %', operation;
|
||||
END IF;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RAISE EXCEPTION 'Failed to process the row: %', SQLERRM;
|
||||
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 $$
|
||||
/*
|
||||
Builds a sql string that, if executed, creates a prepared statement to
|
||||
tests retrive a row from *entity* by its primary key columns.
|
||||
Example
|
||||
select realtime.build_prepared_statement_sql('public.notes', '{"id"}'::text[], '{"bigint"}'::text[])
|
||||
*/
|
||||
select
|
||||
'prepare ' || prepared_statement_name || ' as
|
||||
select
|
||||
exists(
|
||||
select
|
||||
1
|
||||
from
|
||||
' || entity || '
|
||||
where
|
||||
' || string_agg(quote_ident(pkc.name) || '=' || quote_nullable(pkc.value #>> '{}') , ' and ') || '
|
||||
)'
|
||||
from
|
||||
unnest(columns) pkc
|
||||
where
|
||||
pkc.is_pkey
|
||||
group by
|
||||
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 $$
|
||||
declare
|
||||
res jsonb;
|
||||
begin
|
||||
execute format('select to_jsonb(%L::'|| type_::text || ')', val) into res;
|
||||
return res;
|
||||
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 $$
|
||||
/*
|
||||
Casts *val_1* and *val_2* as type *type_* and check the *op* condition for truthiness
|
||||
*/
|
||||
declare
|
||||
op_symbol text = (
|
||||
case
|
||||
when op = 'eq' then '='
|
||||
when op = 'neq' then '!='
|
||||
when op = 'lt' then '<'
|
||||
when op = 'lte' then '<='
|
||||
when op = 'gt' then '>'
|
||||
when op = 'gte' then '>='
|
||||
when op = 'in' then '= any'
|
||||
else 'UNKNOWN OP'
|
||||
end
|
||||
);
|
||||
res boolean;
|
||||
begin
|
||||
execute format(
|
||||
'select %L::'|| type_::text || ' ' || op_symbol
|
||||
|| ' ( %L::'
|
||||
|| (
|
||||
case
|
||||
when op = 'in' then type_::text || '[]'
|
||||
else type_::text end
|
||||
)
|
||||
|| ')', val_1, val_2) into res;
|
||||
return res;
|
||||
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 $_$
|
||||
/*
|
||||
Should the record be visible (true) or filtered out (false) after *filters* are applied
|
||||
*/
|
||||
select
|
||||
-- Default to allowed when no filters present
|
||||
$2 is null -- no filters. this should not happen because subscriptions has a default
|
||||
or array_length($2, 1) is null -- array length of an empty array is null
|
||||
or bool_and(
|
||||
coalesce(
|
||||
realtime.check_equality_op(
|
||||
op:=f.op,
|
||||
type_:=coalesce(
|
||||
col.type_oid::regtype, -- null when wal2json version <= 2.4
|
||||
col.type_name::regtype
|
||||
),
|
||||
-- cast jsonb to text
|
||||
val_1:=col.value #>> '{}',
|
||||
val_2:=f.value
|
||||
),
|
||||
false -- if null, filter does not match
|
||||
)
|
||||
)
|
||||
from
|
||||
unnest(filters) f
|
||||
join unnest(columns) col
|
||||
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'
|
||||
AS $$
|
||||
with pub as (
|
||||
select
|
||||
concat_ws(
|
||||
',',
|
||||
case when bool_or(pubinsert) then 'insert' else null end,
|
||||
case when bool_or(pubupdate) then 'update' else null end,
|
||||
case when bool_or(pubdelete) then 'delete' else null end
|
||||
) as w2j_actions,
|
||||
coalesce(
|
||||
string_agg(
|
||||
realtime.quote_wal2json(format('%I.%I', schemaname, tablename)::regclass),
|
||||
','
|
||||
) filter (where ppt.tablename is not null and ppt.tablename not like '% %'),
|
||||
''
|
||||
) w2j_add_tables
|
||||
from
|
||||
pg_publication pp
|
||||
left join pg_publication_tables ppt
|
||||
on pp.pubname = ppt.pubname
|
||||
where
|
||||
pp.pubname = publication
|
||||
group by
|
||||
pp.pubname
|
||||
limit 1
|
||||
),
|
||||
w2j as (
|
||||
select
|
||||
x.*, pub.w2j_add_tables
|
||||
from
|
||||
pub,
|
||||
pg_logical_slot_get_changes(
|
||||
slot_name, null, max_changes,
|
||||
'include-pk', 'true',
|
||||
'include-transaction', 'false',
|
||||
'include-timestamp', 'true',
|
||||
'include-type-oids', 'true',
|
||||
'format-version', '2',
|
||||
'actions', pub.w2j_actions,
|
||||
'add-tables', pub.w2j_add_tables
|
||||
) x
|
||||
)
|
||||
select
|
||||
xyz.wal,
|
||||
xyz.is_rls_enabled,
|
||||
xyz.subscription_ids,
|
||||
xyz.errors
|
||||
from
|
||||
w2j,
|
||||
realtime.apply_rls(
|
||||
wal := w2j.data::jsonb,
|
||||
max_record_bytes := max_record_bytes
|
||||
) xyz(wal, is_rls_enabled, subscription_ids, errors)
|
||||
where
|
||||
w2j.w2j_add_tables <> ''
|
||||
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 $$
|
||||
select
|
||||
(
|
||||
select string_agg('' || ch,'')
|
||||
from unnest(string_to_array(nsp.nspname::text, null)) with ordinality x(ch, idx)
|
||||
where
|
||||
not (x.idx = 1 and x.ch = '"')
|
||||
and not (
|
||||
x.idx = array_length(string_to_array(nsp.nspname::text, null), 1)
|
||||
and x.ch = '"'
|
||||
)
|
||||
)
|
||||
|| '.'
|
||||
|| (
|
||||
select string_agg('' || ch,'')
|
||||
from unnest(string_to_array(pc.relname::text, null)) with ordinality x(ch, idx)
|
||||
where
|
||||
not (x.idx = 1 and x.ch = '"')
|
||||
and not (
|
||||
x.idx = array_length(string_to_array(nsp.nspname::text, null), 1)
|
||||
and x.ch = '"'
|
||||
)
|
||||
)
|
||||
from
|
||||
pg_class pc
|
||||
join pg_namespace nsp
|
||||
on pc.relnamespace = nsp.oid
|
||||
where
|
||||
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 $$
|
||||
DECLARE
|
||||
generated_id uuid;
|
||||
final_payload jsonb;
|
||||
BEGIN
|
||||
BEGIN
|
||||
-- Generate a new UUID for the id
|
||||
generated_id := gen_random_uuid();
|
||||
|
||||
-- Check if payload has an 'id' key, if not, add the generated UUID
|
||||
IF payload ? 'id' THEN
|
||||
final_payload := payload;
|
||||
ELSE
|
||||
final_payload := jsonb_set(payload, '{id}', to_jsonb(generated_id));
|
||||
END IF;
|
||||
|
||||
-- Set the topic configuration
|
||||
EXECUTE format('SET LOCAL realtime.topic TO %L', topic);
|
||||
|
||||
-- Attempt to insert the message
|
||||
INSERT INTO realtime.messages (id, payload, event, topic, private, extension)
|
||||
VALUES (generated_id, final_payload, event, topic, private, 'broadcast');
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
-- Capture and notify the error
|
||||
RAISE WARNING 'ErrorSendingBroadcastMessage: %', SQLERRM;
|
||||
END;
|
||||
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 $$
|
||||
/*
|
||||
Validates that the user defined filters for a subscription:
|
||||
- refer to valid columns that the claimed role may access
|
||||
- values are coercable to the correct column type
|
||||
*/
|
||||
declare
|
||||
col_names text[] = coalesce(
|
||||
array_agg(c.column_name order by c.ordinal_position),
|
||||
'{}'::text[]
|
||||
)
|
||||
from
|
||||
information_schema.columns c
|
||||
where
|
||||
format('%I.%I', c.table_schema, c.table_name)::regclass = new.entity
|
||||
and pg_catalog.has_column_privilege(
|
||||
(new.claims ->> 'role'),
|
||||
format('%I.%I', c.table_schema, c.table_name)::regclass,
|
||||
c.column_name,
|
||||
'SELECT'
|
||||
);
|
||||
filter realtime.user_defined_filter;
|
||||
col_type regtype;
|
||||
|
||||
in_val jsonb;
|
||||
begin
|
||||
for filter in select * from unnest(new.filters) loop
|
||||
-- Filtered column is valid
|
||||
if not filter.column_name = any(col_names) then
|
||||
raise exception 'invalid column for filter %', filter.column_name;
|
||||
end if;
|
||||
|
||||
-- Type is sanitized and safe for string interpolation
|
||||
col_type = (
|
||||
select atttypid::regtype
|
||||
from pg_catalog.pg_attribute
|
||||
where attrelid = new.entity
|
||||
and attname = filter.column_name
|
||||
);
|
||||
if col_type is null then
|
||||
raise exception 'failed to lookup type for column %', filter.column_name;
|
||||
end if;
|
||||
|
||||
-- Set maximum number of entries for in filter
|
||||
if filter.op = 'in'::realtime.equality_op then
|
||||
in_val = realtime.cast(filter.value, (col_type::text || '[]')::regtype);
|
||||
if coalesce(jsonb_array_length(in_val), 0) > 100 then
|
||||
raise exception 'too many values for `in` filter. Maximum 100';
|
||||
end if;
|
||||
else
|
||||
-- raises an exception if value is not coercable to type
|
||||
perform realtime.cast(filter.value, col_type);
|
||||
end if;
|
||||
|
||||
end loop;
|
||||
|
||||
-- Apply consistent order to filters so the unique constraint on
|
||||
-- (subscription_id, entity, filters) can't be tricked by a different filter order
|
||||
new.filters = coalesce(
|
||||
array_agg(f order by f.column_name, f.op, f.value),
|
||||
'{}'
|
||||
) from unnest(new.filters) f;
|
||||
|
||||
return new;
|
||||
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
|
||||
--
|
||||
|
||||
877
database-novo/schema/03_functions/storage.sql
Normal file
877
database-novo/schema/03_functions/storage.sql
Normal file
@@ -0,0 +1,877 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Functions — storage schema
|
||||
-- =============================================================================
|
||||
|
||||
CREATE FUNCTION storage.can_insert_object(bucketid text, name text, owner uuid, metadata jsonb) RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO "storage"."objects" ("bucket_id", "name", "owner", "metadata") VALUES (bucketid, name, owner, metadata);
|
||||
-- hack to rollback the successful insert
|
||||
RAISE sqlstate 'PT200' using
|
||||
message = 'ROLLBACK',
|
||||
detail = 'rollback successful insert';
|
||||
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 $$
|
||||
begin
|
||||
if length(new.name) > 100 then
|
||||
raise exception 'bucket name "%" is too long (% characters). Max is 100.', new.name, length(new.name);
|
||||
end if;
|
||||
return new;
|
||||
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 $$
|
||||
DECLARE
|
||||
_parts text[];
|
||||
_filename text;
|
||||
BEGIN
|
||||
select string_to_array(name, '/') into _parts;
|
||||
select _parts[array_length(_parts,1)] into _filename;
|
||||
-- @todo return the last part instead of 2
|
||||
return reverse(split_part(reverse(_filename), '.', 1));
|
||||
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 $$
|
||||
DECLARE
|
||||
_parts text[];
|
||||
BEGIN
|
||||
select string_to_array(name, '/') into _parts;
|
||||
return _parts[array_length(_parts,1)];
|
||||
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 $$
|
||||
DECLARE
|
||||
_parts text[];
|
||||
BEGIN
|
||||
select string_to_array(name, '/') into _parts;
|
||||
return _parts[1:array_length(_parts,1)-1];
|
||||
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 $$
|
||||
SELECT CASE
|
||||
WHEN position(p_delimiter IN substring(p_key FROM length(p_prefix) + 1)) > 0
|
||||
THEN left(p_key, length(p_prefix) + position(p_delimiter IN substring(p_key FROM length(p_prefix) + 1)))
|
||||
ELSE NULL
|
||||
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 $$
|
||||
BEGIN
|
||||
return query
|
||||
select sum((metadata->>'size')::int) as size, obj.bucket_id
|
||||
from "storage".objects as obj
|
||||
group by obj.bucket_id;
|
||||
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 $_$
|
||||
BEGIN
|
||||
RETURN QUERY EXECUTE
|
||||
'SELECT DISTINCT ON(key COLLATE "C") * from (
|
||||
SELECT
|
||||
CASE
|
||||
WHEN position($2 IN substring(key from length($1) + 1)) > 0 THEN
|
||||
substring(key from 1 for length($1) + position($2 IN substring(key from length($1) + 1)))
|
||||
ELSE
|
||||
key
|
||||
END AS key, id, created_at
|
||||
FROM
|
||||
storage.s3_multipart_uploads
|
||||
WHERE
|
||||
bucket_id = $5 AND
|
||||
key ILIKE $1 || ''%'' AND
|
||||
CASE
|
||||
WHEN $4 != '''' AND $6 = '''' THEN
|
||||
CASE
|
||||
WHEN position($2 IN substring(key from length($1) + 1)) > 0 THEN
|
||||
substring(key from 1 for length($1) + position($2 IN substring(key from length($1) + 1))) COLLATE "C" > $4
|
||||
ELSE
|
||||
key COLLATE "C" > $4
|
||||
END
|
||||
ELSE
|
||||
true
|
||||
END AND
|
||||
CASE
|
||||
WHEN $6 != '''' THEN
|
||||
id COLLATE "C" > $6
|
||||
ELSE
|
||||
true
|
||||
END
|
||||
ORDER BY
|
||||
key COLLATE "C" ASC, created_at ASC) as e order by key COLLATE "C" LIMIT $3'
|
||||
USING prefix_param, delimiter_param, max_keys, next_key_token, bucket_id, next_upload_token;
|
||||
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 $_$
|
||||
DECLARE
|
||||
v_peek_name TEXT;
|
||||
v_current RECORD;
|
||||
v_common_prefix TEXT;
|
||||
|
||||
-- Configuration
|
||||
v_is_asc BOOLEAN;
|
||||
v_prefix TEXT;
|
||||
v_start TEXT;
|
||||
v_upper_bound TEXT;
|
||||
v_file_batch_size INT;
|
||||
|
||||
-- Seek state
|
||||
v_next_seek TEXT;
|
||||
v_count INT := 0;
|
||||
|
||||
-- Dynamic SQL for batch query only
|
||||
v_batch_query TEXT;
|
||||
|
||||
BEGIN
|
||||
-- ========================================================================
|
||||
-- INITIALIZATION
|
||||
-- ========================================================================
|
||||
v_is_asc := lower(coalesce(sort_order, 'asc')) = 'asc';
|
||||
v_prefix := coalesce(prefix_param, '');
|
||||
v_start := CASE WHEN coalesce(next_token, '') <> '' THEN next_token ELSE coalesce(start_after, '') END;
|
||||
v_file_batch_size := LEAST(GREATEST(max_keys * 2, 100), 1000);
|
||||
|
||||
-- Calculate upper bound for prefix filtering (bytewise, using COLLATE "C")
|
||||
IF v_prefix = '' THEN
|
||||
v_upper_bound := NULL;
|
||||
ELSIF right(v_prefix, 1) = delimiter_param THEN
|
||||
v_upper_bound := left(v_prefix, -1) || chr(ascii(delimiter_param) + 1);
|
||||
ELSE
|
||||
v_upper_bound := left(v_prefix, -1) || chr(ascii(right(v_prefix, 1)) + 1);
|
||||
END IF;
|
||||
|
||||
-- Build batch query (dynamic SQL - called infrequently, amortized over many rows)
|
||||
IF v_is_asc THEN
|
||||
IF v_upper_bound IS NOT NULL THEN
|
||||
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||
'FROM storage.objects o WHERE o.bucket_id = $1 AND o.name COLLATE "C" >= $2 ' ||
|
||||
'AND o.name COLLATE "C" < $3 ORDER BY o.name COLLATE "C" ASC LIMIT $4';
|
||||
ELSE
|
||||
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||
'FROM storage.objects o WHERE o.bucket_id = $1 AND o.name COLLATE "C" >= $2 ' ||
|
||||
'ORDER BY o.name COLLATE "C" ASC LIMIT $4';
|
||||
END IF;
|
||||
ELSE
|
||||
IF v_upper_bound IS NOT NULL THEN
|
||||
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||
'FROM storage.objects o WHERE o.bucket_id = $1 AND o.name COLLATE "C" < $2 ' ||
|
||||
'AND o.name COLLATE "C" >= $3 ORDER BY o.name COLLATE "C" DESC LIMIT $4';
|
||||
ELSE
|
||||
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||
'FROM storage.objects o WHERE o.bucket_id = $1 AND o.name COLLATE "C" < $2 ' ||
|
||||
'ORDER BY o.name COLLATE "C" DESC LIMIT $4';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- ========================================================================
|
||||
-- SEEK INITIALIZATION: Determine starting position
|
||||
-- ========================================================================
|
||||
IF v_start = '' THEN
|
||||
IF v_is_asc THEN
|
||||
v_next_seek := v_prefix;
|
||||
ELSE
|
||||
-- DESC without cursor: find the last item in range
|
||||
IF v_upper_bound IS NOT NULL THEN
|
||||
SELECT o.name INTO v_next_seek FROM storage.objects o
|
||||
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" >= v_prefix AND o.name COLLATE "C" < v_upper_bound
|
||||
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
||||
ELSIF v_prefix <> '' THEN
|
||||
SELECT o.name INTO v_next_seek FROM storage.objects o
|
||||
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" >= v_prefix
|
||||
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
||||
ELSE
|
||||
SELECT o.name INTO v_next_seek FROM storage.objects o
|
||||
WHERE o.bucket_id = _bucket_id
|
||||
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
||||
END IF;
|
||||
|
||||
IF v_next_seek IS NOT NULL THEN
|
||||
v_next_seek := v_next_seek || delimiter_param;
|
||||
ELSE
|
||||
RETURN;
|
||||
END IF;
|
||||
END IF;
|
||||
ELSE
|
||||
-- Cursor provided: determine if it refers to a folder or leaf
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM storage.objects o
|
||||
WHERE o.bucket_id = _bucket_id
|
||||
AND o.name COLLATE "C" LIKE v_start || delimiter_param || '%'
|
||||
LIMIT 1
|
||||
) THEN
|
||||
-- Cursor refers to a folder
|
||||
IF v_is_asc THEN
|
||||
v_next_seek := v_start || chr(ascii(delimiter_param) + 1);
|
||||
ELSE
|
||||
v_next_seek := v_start || delimiter_param;
|
||||
END IF;
|
||||
ELSE
|
||||
-- Cursor refers to a leaf object
|
||||
IF v_is_asc THEN
|
||||
v_next_seek := v_start || delimiter_param;
|
||||
ELSE
|
||||
v_next_seek := v_start;
|
||||
END IF;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- ========================================================================
|
||||
-- MAIN LOOP: Hybrid peek-then-batch algorithm
|
||||
-- Uses STATIC SQL for peek (hot path) and DYNAMIC SQL for batch
|
||||
-- ========================================================================
|
||||
LOOP
|
||||
EXIT WHEN v_count >= max_keys;
|
||||
|
||||
-- STEP 1: PEEK using STATIC SQL (plan cached, very fast)
|
||||
IF v_is_asc THEN
|
||||
IF v_upper_bound IS NOT NULL THEN
|
||||
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" >= v_next_seek AND o.name COLLATE "C" < v_upper_bound
|
||||
ORDER BY o.name COLLATE "C" ASC LIMIT 1;
|
||||
ELSE
|
||||
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" >= v_next_seek
|
||||
ORDER BY o.name COLLATE "C" ASC LIMIT 1;
|
||||
END IF;
|
||||
ELSE
|
||||
IF v_upper_bound IS NOT NULL THEN
|
||||
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" < v_next_seek AND o.name COLLATE "C" >= v_prefix
|
||||
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
||||
ELSIF v_prefix <> '' THEN
|
||||
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" < v_next_seek AND o.name COLLATE "C" >= v_prefix
|
||||
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
||||
ELSE
|
||||
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" < v_next_seek
|
||||
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
EXIT WHEN v_peek_name IS NULL;
|
||||
|
||||
-- STEP 2: Check if this is a FOLDER or FILE
|
||||
v_common_prefix := storage.get_common_prefix(v_peek_name, v_prefix, delimiter_param);
|
||||
|
||||
IF v_common_prefix IS NOT NULL THEN
|
||||
-- FOLDER: Emit and skip to next folder (no heap access needed)
|
||||
name := rtrim(v_common_prefix, delimiter_param);
|
||||
id := NULL;
|
||||
updated_at := NULL;
|
||||
created_at := NULL;
|
||||
last_accessed_at := NULL;
|
||||
metadata := NULL;
|
||||
RETURN NEXT;
|
||||
v_count := v_count + 1;
|
||||
|
||||
-- Advance seek past the folder range
|
||||
IF v_is_asc THEN
|
||||
v_next_seek := left(v_common_prefix, -1) || chr(ascii(delimiter_param) + 1);
|
||||
ELSE
|
||||
v_next_seek := v_common_prefix;
|
||||
END IF;
|
||||
ELSE
|
||||
-- FILE: Batch fetch using DYNAMIC SQL (overhead amortized over many rows)
|
||||
-- For ASC: upper_bound is the exclusive upper limit (< condition)
|
||||
-- For DESC: prefix is the inclusive lower limit (>= condition)
|
||||
FOR v_current IN EXECUTE v_batch_query USING _bucket_id, v_next_seek,
|
||||
CASE WHEN v_is_asc THEN COALESCE(v_upper_bound, v_prefix) ELSE v_prefix END, v_file_batch_size
|
||||
LOOP
|
||||
v_common_prefix := storage.get_common_prefix(v_current.name, v_prefix, delimiter_param);
|
||||
|
||||
IF v_common_prefix IS NOT NULL THEN
|
||||
-- Hit a folder: exit batch, let peek handle it
|
||||
v_next_seek := v_current.name;
|
||||
EXIT;
|
||||
END IF;
|
||||
|
||||
-- Emit file
|
||||
name := v_current.name;
|
||||
id := v_current.id;
|
||||
updated_at := v_current.updated_at;
|
||||
created_at := v_current.created_at;
|
||||
last_accessed_at := v_current.last_accessed_at;
|
||||
metadata := v_current.metadata;
|
||||
RETURN NEXT;
|
||||
v_count := v_count + 1;
|
||||
|
||||
-- Advance seek past this file
|
||||
IF v_is_asc THEN
|
||||
v_next_seek := v_current.name || delimiter_param;
|
||||
ELSE
|
||||
v_next_seek := v_current.name;
|
||||
END IF;
|
||||
|
||||
EXIT WHEN v_count >= max_keys;
|
||||
END LOOP;
|
||||
END IF;
|
||||
END LOOP;
|
||||
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 $$
|
||||
BEGIN
|
||||
RETURN current_setting('storage.operation', true);
|
||||
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 $$
|
||||
BEGIN
|
||||
-- Check if storage.allow_delete_query is set to 'true'
|
||||
IF COALESCE(current_setting('storage.allow_delete_query', true), 'false') != 'true' THEN
|
||||
RAISE EXCEPTION 'Direct deletion from storage tables is not allowed. Use the Storage API instead.'
|
||||
USING HINT = 'This prevents accidental data loss from orphaned objects.',
|
||||
ERRCODE = '42501';
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
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 $_$
|
||||
DECLARE
|
||||
v_peek_name TEXT;
|
||||
v_current RECORD;
|
||||
v_common_prefix TEXT;
|
||||
v_delimiter CONSTANT TEXT := '/';
|
||||
|
||||
-- Configuration
|
||||
v_limit INT;
|
||||
v_prefix TEXT;
|
||||
v_prefix_lower TEXT;
|
||||
v_is_asc BOOLEAN;
|
||||
v_order_by TEXT;
|
||||
v_sort_order TEXT;
|
||||
v_upper_bound TEXT;
|
||||
v_file_batch_size INT;
|
||||
|
||||
-- Dynamic SQL for batch query only
|
||||
v_batch_query TEXT;
|
||||
|
||||
-- Seek state
|
||||
v_next_seek TEXT;
|
||||
v_count INT := 0;
|
||||
v_skipped INT := 0;
|
||||
BEGIN
|
||||
-- ========================================================================
|
||||
-- INITIALIZATION
|
||||
-- ========================================================================
|
||||
v_limit := LEAST(coalesce(limits, 100), 1500);
|
||||
v_prefix := coalesce(prefix, '') || coalesce(search, '');
|
||||
v_prefix_lower := lower(v_prefix);
|
||||
v_is_asc := lower(coalesce(sortorder, 'asc')) = 'asc';
|
||||
v_file_batch_size := LEAST(GREATEST(v_limit * 2, 100), 1000);
|
||||
|
||||
-- Validate sort column
|
||||
CASE lower(coalesce(sortcolumn, 'name'))
|
||||
WHEN 'name' THEN v_order_by := 'name';
|
||||
WHEN 'updated_at' THEN v_order_by := 'updated_at';
|
||||
WHEN 'created_at' THEN v_order_by := 'created_at';
|
||||
WHEN 'last_accessed_at' THEN v_order_by := 'last_accessed_at';
|
||||
ELSE v_order_by := 'name';
|
||||
END CASE;
|
||||
|
||||
v_sort_order := CASE WHEN v_is_asc THEN 'asc' ELSE 'desc' END;
|
||||
|
||||
-- ========================================================================
|
||||
-- NON-NAME SORTING: Use path_tokens approach (unchanged)
|
||||
-- ========================================================================
|
||||
IF v_order_by != 'name' THEN
|
||||
RETURN QUERY EXECUTE format(
|
||||
$sql$
|
||||
WITH folders AS (
|
||||
SELECT path_tokens[$1] AS folder
|
||||
FROM storage.objects
|
||||
WHERE objects.name ILIKE $2 || '%%'
|
||||
AND bucket_id = $3
|
||||
AND array_length(objects.path_tokens, 1) <> $1
|
||||
GROUP BY folder
|
||||
ORDER BY folder %s
|
||||
)
|
||||
(SELECT folder AS "name",
|
||||
NULL::uuid AS id,
|
||||
NULL::timestamptz AS updated_at,
|
||||
NULL::timestamptz AS created_at,
|
||||
NULL::timestamptz AS last_accessed_at,
|
||||
NULL::jsonb AS metadata FROM folders)
|
||||
UNION ALL
|
||||
(SELECT path_tokens[$1] AS "name",
|
||||
id, updated_at, created_at, last_accessed_at, metadata
|
||||
FROM storage.objects
|
||||
WHERE objects.name ILIKE $2 || '%%'
|
||||
AND bucket_id = $3
|
||||
AND array_length(objects.path_tokens, 1) = $1
|
||||
ORDER BY %I %s)
|
||||
LIMIT $4 OFFSET $5
|
||||
$sql$, v_sort_order, v_order_by, v_sort_order
|
||||
) USING levels, v_prefix, bucketname, v_limit, offsets;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- ========================================================================
|
||||
-- NAME SORTING: Hybrid skip-scan with batch optimization
|
||||
-- ========================================================================
|
||||
|
||||
-- Calculate upper bound for prefix filtering
|
||||
IF v_prefix_lower = '' THEN
|
||||
v_upper_bound := NULL;
|
||||
ELSIF right(v_prefix_lower, 1) = v_delimiter THEN
|
||||
v_upper_bound := left(v_prefix_lower, -1) || chr(ascii(v_delimiter) + 1);
|
||||
ELSE
|
||||
v_upper_bound := left(v_prefix_lower, -1) || chr(ascii(right(v_prefix_lower, 1)) + 1);
|
||||
END IF;
|
||||
|
||||
-- Build batch query (dynamic SQL - called infrequently, amortized over many rows)
|
||||
IF v_is_asc THEN
|
||||
IF v_upper_bound IS NOT NULL THEN
|
||||
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||
'FROM storage.objects o WHERE o.bucket_id = $1 AND lower(o.name) COLLATE "C" >= $2 ' ||
|
||||
'AND lower(o.name) COLLATE "C" < $3 ORDER BY lower(o.name) COLLATE "C" ASC LIMIT $4';
|
||||
ELSE
|
||||
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||
'FROM storage.objects o WHERE o.bucket_id = $1 AND lower(o.name) COLLATE "C" >= $2 ' ||
|
||||
'ORDER BY lower(o.name) COLLATE "C" ASC LIMIT $4';
|
||||
END IF;
|
||||
ELSE
|
||||
IF v_upper_bound IS NOT NULL THEN
|
||||
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||
'FROM storage.objects o WHERE o.bucket_id = $1 AND lower(o.name) COLLATE "C" < $2 ' ||
|
||||
'AND lower(o.name) COLLATE "C" >= $3 ORDER BY lower(o.name) COLLATE "C" DESC LIMIT $4';
|
||||
ELSE
|
||||
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
||||
'FROM storage.objects o WHERE o.bucket_id = $1 AND lower(o.name) COLLATE "C" < $2 ' ||
|
||||
'ORDER BY lower(o.name) COLLATE "C" DESC LIMIT $4';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Initialize seek position
|
||||
IF v_is_asc THEN
|
||||
v_next_seek := v_prefix_lower;
|
||||
ELSE
|
||||
-- DESC: find the last item in range first (static SQL)
|
||||
IF v_upper_bound IS NOT NULL THEN
|
||||
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" >= v_prefix_lower AND lower(o.name) COLLATE "C" < v_upper_bound
|
||||
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
||||
ELSIF v_prefix_lower <> '' THEN
|
||||
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" >= v_prefix_lower
|
||||
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
||||
ELSE
|
||||
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||
WHERE o.bucket_id = bucketname
|
||||
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
||||
END IF;
|
||||
|
||||
IF v_peek_name IS NOT NULL THEN
|
||||
v_next_seek := lower(v_peek_name) || v_delimiter;
|
||||
ELSE
|
||||
RETURN;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- ========================================================================
|
||||
-- MAIN LOOP: Hybrid peek-then-batch algorithm
|
||||
-- Uses STATIC SQL for peek (hot path) and DYNAMIC SQL for batch
|
||||
-- ========================================================================
|
||||
LOOP
|
||||
EXIT WHEN v_count >= v_limit;
|
||||
|
||||
-- STEP 1: PEEK using STATIC SQL (plan cached, very fast)
|
||||
IF v_is_asc THEN
|
||||
IF v_upper_bound IS NOT NULL THEN
|
||||
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" >= v_next_seek AND lower(o.name) COLLATE "C" < v_upper_bound
|
||||
ORDER BY lower(o.name) COLLATE "C" ASC LIMIT 1;
|
||||
ELSE
|
||||
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" >= v_next_seek
|
||||
ORDER BY lower(o.name) COLLATE "C" ASC LIMIT 1;
|
||||
END IF;
|
||||
ELSE
|
||||
IF v_upper_bound IS NOT NULL THEN
|
||||
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" < v_next_seek AND lower(o.name) COLLATE "C" >= v_prefix_lower
|
||||
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
||||
ELSIF v_prefix_lower <> '' THEN
|
||||
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" < v_next_seek AND lower(o.name) COLLATE "C" >= v_prefix_lower
|
||||
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
||||
ELSE
|
||||
SELECT o.name INTO v_peek_name FROM storage.objects o
|
||||
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" < v_next_seek
|
||||
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
EXIT WHEN v_peek_name IS NULL;
|
||||
|
||||
-- STEP 2: Check if this is a FOLDER or FILE
|
||||
v_common_prefix := storage.get_common_prefix(lower(v_peek_name), v_prefix_lower, v_delimiter);
|
||||
|
||||
IF v_common_prefix IS NOT NULL THEN
|
||||
-- FOLDER: Handle offset, emit if needed, skip to next folder
|
||||
IF v_skipped < offsets THEN
|
||||
v_skipped := v_skipped + 1;
|
||||
ELSE
|
||||
name := split_part(rtrim(v_common_prefix, v_delimiter), v_delimiter, levels);
|
||||
id := NULL;
|
||||
updated_at := NULL;
|
||||
created_at := NULL;
|
||||
last_accessed_at := NULL;
|
||||
metadata := NULL;
|
||||
RETURN NEXT;
|
||||
v_count := v_count + 1;
|
||||
END IF;
|
||||
|
||||
-- Advance seek past the folder range
|
||||
IF v_is_asc THEN
|
||||
v_next_seek := lower(left(v_common_prefix, -1)) || chr(ascii(v_delimiter) + 1);
|
||||
ELSE
|
||||
v_next_seek := lower(v_common_prefix);
|
||||
END IF;
|
||||
ELSE
|
||||
-- FILE: Batch fetch using DYNAMIC SQL (overhead amortized over many rows)
|
||||
-- For ASC: upper_bound is the exclusive upper limit (< condition)
|
||||
-- For DESC: prefix_lower is the inclusive lower limit (>= condition)
|
||||
FOR v_current IN EXECUTE v_batch_query
|
||||
USING bucketname, v_next_seek,
|
||||
CASE WHEN v_is_asc THEN COALESCE(v_upper_bound, v_prefix_lower) ELSE v_prefix_lower END, v_file_batch_size
|
||||
LOOP
|
||||
v_common_prefix := storage.get_common_prefix(lower(v_current.name), v_prefix_lower, v_delimiter);
|
||||
|
||||
IF v_common_prefix IS NOT NULL THEN
|
||||
-- Hit a folder: exit batch, let peek handle it
|
||||
v_next_seek := lower(v_current.name);
|
||||
EXIT;
|
||||
END IF;
|
||||
|
||||
-- Handle offset skipping
|
||||
IF v_skipped < offsets THEN
|
||||
v_skipped := v_skipped + 1;
|
||||
ELSE
|
||||
-- Emit file
|
||||
name := split_part(v_current.name, v_delimiter, levels);
|
||||
id := v_current.id;
|
||||
updated_at := v_current.updated_at;
|
||||
created_at := v_current.created_at;
|
||||
last_accessed_at := v_current.last_accessed_at;
|
||||
metadata := v_current.metadata;
|
||||
RETURN NEXT;
|
||||
v_count := v_count + 1;
|
||||
END IF;
|
||||
|
||||
-- Advance seek past this file
|
||||
IF v_is_asc THEN
|
||||
v_next_seek := lower(v_current.name) || v_delimiter;
|
||||
ELSE
|
||||
v_next_seek := lower(v_current.name);
|
||||
END IF;
|
||||
|
||||
EXIT WHEN v_count >= v_limit;
|
||||
END LOOP;
|
||||
END IF;
|
||||
END LOOP;
|
||||
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 $_$
|
||||
DECLARE
|
||||
v_cursor_op text;
|
||||
v_query text;
|
||||
v_prefix text;
|
||||
BEGIN
|
||||
v_prefix := coalesce(p_prefix, '');
|
||||
|
||||
IF p_sort_order = 'asc' THEN
|
||||
v_cursor_op := '>';
|
||||
ELSE
|
||||
v_cursor_op := '<';
|
||||
END IF;
|
||||
|
||||
v_query := format($sql$
|
||||
WITH raw_objects AS (
|
||||
SELECT
|
||||
o.name AS obj_name,
|
||||
o.id AS obj_id,
|
||||
o.updated_at AS obj_updated_at,
|
||||
o.created_at AS obj_created_at,
|
||||
o.last_accessed_at AS obj_last_accessed_at,
|
||||
o.metadata AS obj_metadata,
|
||||
storage.get_common_prefix(o.name, $1, '/') AS common_prefix
|
||||
FROM storage.objects o
|
||||
WHERE o.bucket_id = $2
|
||||
AND o.name COLLATE "C" LIKE $1 || '%%'
|
||||
),
|
||||
-- Aggregate common prefixes (folders)
|
||||
-- Both created_at and updated_at use MIN(obj_created_at) to match the old prefixes table behavior
|
||||
aggregated_prefixes AS (
|
||||
SELECT
|
||||
rtrim(common_prefix, '/') AS name,
|
||||
NULL::uuid AS id,
|
||||
MIN(obj_created_at) AS updated_at,
|
||||
MIN(obj_created_at) AS created_at,
|
||||
NULL::timestamptz AS last_accessed_at,
|
||||
NULL::jsonb AS metadata,
|
||||
TRUE AS is_prefix
|
||||
FROM raw_objects
|
||||
WHERE common_prefix IS NOT NULL
|
||||
GROUP BY common_prefix
|
||||
),
|
||||
leaf_objects AS (
|
||||
SELECT
|
||||
obj_name AS name,
|
||||
obj_id AS id,
|
||||
obj_updated_at AS updated_at,
|
||||
obj_created_at AS created_at,
|
||||
obj_last_accessed_at AS last_accessed_at,
|
||||
obj_metadata AS metadata,
|
||||
FALSE AS is_prefix
|
||||
FROM raw_objects
|
||||
WHERE common_prefix IS NULL
|
||||
),
|
||||
combined AS (
|
||||
SELECT * FROM aggregated_prefixes
|
||||
UNION ALL
|
||||
SELECT * FROM leaf_objects
|
||||
),
|
||||
filtered AS (
|
||||
SELECT *
|
||||
FROM combined
|
||||
WHERE (
|
||||
$5 = ''
|
||||
OR ROW(
|
||||
date_trunc('milliseconds', %I),
|
||||
name COLLATE "C"
|
||||
) %s ROW(
|
||||
COALESCE(NULLIF($6, '')::timestamptz, 'epoch'::timestamptz),
|
||||
$5
|
||||
)
|
||||
)
|
||||
)
|
||||
SELECT
|
||||
split_part(name, '/', $3) AS key,
|
||||
name,
|
||||
id,
|
||||
updated_at,
|
||||
created_at,
|
||||
last_accessed_at,
|
||||
metadata
|
||||
FROM filtered
|
||||
ORDER BY
|
||||
COALESCE(date_trunc('milliseconds', %I), 'epoch'::timestamptz) %s,
|
||||
name COLLATE "C" %s
|
||||
LIMIT $4
|
||||
$sql$,
|
||||
p_sort_column,
|
||||
v_cursor_op,
|
||||
p_sort_column,
|
||||
p_sort_order,
|
||||
p_sort_order
|
||||
);
|
||||
|
||||
RETURN QUERY EXECUTE v_query
|
||||
USING v_prefix, p_bucket_id, p_level, p_limit, p_start_after, p_sort_column_after;
|
||||
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 $$
|
||||
DECLARE
|
||||
v_sort_col text;
|
||||
v_sort_ord text;
|
||||
v_limit int;
|
||||
BEGIN
|
||||
-- Cap limit to maximum of 1500 records
|
||||
v_limit := LEAST(coalesce(limits, 100), 1500);
|
||||
|
||||
-- Validate and normalize sort_order
|
||||
v_sort_ord := lower(coalesce(sort_order, 'asc'));
|
||||
IF v_sort_ord NOT IN ('asc', 'desc') THEN
|
||||
v_sort_ord := 'asc';
|
||||
END IF;
|
||||
|
||||
-- Validate and normalize sort_column
|
||||
v_sort_col := lower(coalesce(sort_column, 'name'));
|
||||
IF v_sort_col NOT IN ('name', 'updated_at', 'created_at') THEN
|
||||
v_sort_col := 'name';
|
||||
END IF;
|
||||
|
||||
-- Route to appropriate implementation
|
||||
IF v_sort_col = 'name' THEN
|
||||
-- Use list_objects_with_delimiter for name sorting (most efficient: O(k * log n))
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
split_part(l.name, '/', levels) AS key,
|
||||
l.name AS name,
|
||||
l.id,
|
||||
l.updated_at,
|
||||
l.created_at,
|
||||
l.last_accessed_at,
|
||||
l.metadata
|
||||
FROM storage.list_objects_with_delimiter(
|
||||
bucket_name,
|
||||
coalesce(prefix, ''),
|
||||
'/',
|
||||
v_limit,
|
||||
start_after,
|
||||
'',
|
||||
v_sort_ord
|
||||
) l;
|
||||
ELSE
|
||||
-- Use aggregation approach for timestamp sorting
|
||||
-- Not efficient for large datasets but supports correct pagination
|
||||
RETURN QUERY SELECT * FROM storage.search_by_timestamp(
|
||||
prefix, bucket_name, v_limit, levels, start_after,
|
||||
v_sort_ord, v_sort_col, sort_column_after
|
||||
);
|
||||
END IF;
|
||||
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 $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
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
|
||||
--
|
||||
|
||||
87
database-novo/schema/03_functions/supabase_functions.sql
Normal file
87
database-novo/schema/03_functions/supabase_functions.sql
Normal file
@@ -0,0 +1,87 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Functions — supabase_functions schema
|
||||
-- =============================================================================
|
||||
|
||||
CREATE FUNCTION supabase_functions.http_request() RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
SET search_path TO 'supabase_functions'
|
||||
AS $$
|
||||
DECLARE
|
||||
request_id bigint;
|
||||
payload jsonb;
|
||||
url text := TG_ARGV[0]::text;
|
||||
method text := TG_ARGV[1]::text;
|
||||
headers jsonb DEFAULT '{}'::jsonb;
|
||||
params jsonb DEFAULT '{}'::jsonb;
|
||||
timeout_ms integer DEFAULT 1000;
|
||||
BEGIN
|
||||
IF url IS NULL OR url = 'null' THEN
|
||||
RAISE EXCEPTION 'url argument is missing';
|
||||
END IF;
|
||||
|
||||
IF method IS NULL OR method = 'null' THEN
|
||||
RAISE EXCEPTION 'method argument is missing';
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN
|
||||
headers = '{"Content-Type": "application/json"}'::jsonb;
|
||||
ELSE
|
||||
headers = TG_ARGV[2]::jsonb;
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN
|
||||
params = '{}'::jsonb;
|
||||
ELSE
|
||||
params = TG_ARGV[3]::jsonb;
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN
|
||||
timeout_ms = 1000;
|
||||
ELSE
|
||||
timeout_ms = TG_ARGV[4]::integer;
|
||||
END IF;
|
||||
|
||||
CASE
|
||||
WHEN method = 'GET' THEN
|
||||
SELECT http_get INTO request_id FROM net.http_get(
|
||||
url,
|
||||
params,
|
||||
headers,
|
||||
timeout_ms
|
||||
);
|
||||
WHEN method = 'POST' THEN
|
||||
payload = jsonb_build_object(
|
||||
'old_record', OLD,
|
||||
'record', NEW,
|
||||
'type', TG_OP,
|
||||
'table', TG_TABLE_NAME,
|
||||
'schema', TG_TABLE_SCHEMA
|
||||
);
|
||||
|
||||
SELECT http_post INTO request_id FROM net.http_post(
|
||||
url,
|
||||
payload,
|
||||
params,
|
||||
headers,
|
||||
timeout_ms
|
||||
);
|
||||
ELSE
|
||||
RAISE EXCEPTION 'method argument % is invalid', method;
|
||||
END CASE;
|
||||
|
||||
INSERT INTO supabase_functions.hooks
|
||||
(hook_table_id, hook_name, request_id)
|
||||
VALUES
|
||||
(TG_RELID, TG_NAME, request_id);
|
||||
|
||||
RETURN NEW;
|
||||
END
|
||||
$$;
|
||||
|
||||
|
||||
ALTER FUNCTION supabase_functions.http_request() OWNER TO supabase_functions_admin;
|
||||
|
||||
--
|
||||
-- Name: extensions; Type: TABLE; Schema: _realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
Reference in New Issue
Block a user