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:
26057
database-novo/schema/00_full/schema.sql
Normal file
26057
database-novo/schema/00_full/schema.sql
Normal file
File diff suppressed because it is too large
Load Diff
256
database-novo/schema/01_extensions/extensions.sql
Normal file
256
database-novo/schema/01_extensions/extensions.sql
Normal file
@@ -0,0 +1,256 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Extensions e Schemas
|
||||
-- Extraído de schema.sql (2026-03-23)
|
||||
-- =============================================================================
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
\restrict ABfzP9IZJ8pAzvgt6E9jKpFn1phQ3b3Lgk09BZZTle5el6ODr77nIXlXnCf1PS1
|
||||
|
||||
-- Dumped from database version 17.6
|
||||
-- Dumped by pg_dump version 17.6
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET transaction_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
--
|
||||
-- Name: _realtime; Type: SCHEMA; Schema: -; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE SCHEMA _realtime;
|
||||
|
||||
|
||||
ALTER SCHEMA _realtime OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: auth; Type: SCHEMA; Schema: -; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE SCHEMA auth;
|
||||
|
||||
|
||||
ALTER SCHEMA auth OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: pg_cron; Type: EXTENSION; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pg_cron WITH SCHEMA pg_catalog;
|
||||
|
||||
|
||||
--
|
||||
-- Name: EXTENSION pg_cron; Type: COMMENT; Schema: -; Owner:
|
||||
--
|
||||
|
||||
COMMENT ON EXTENSION pg_cron IS 'Job scheduler for PostgreSQL';
|
||||
|
||||
|
||||
--
|
||||
-- Name: extensions; Type: SCHEMA; Schema: -; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE SCHEMA extensions;
|
||||
|
||||
|
||||
ALTER SCHEMA extensions OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: graphql; Type: SCHEMA; Schema: -; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE SCHEMA graphql;
|
||||
|
||||
|
||||
ALTER SCHEMA graphql OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: graphql_public; Type: SCHEMA; Schema: -; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE SCHEMA graphql_public;
|
||||
|
||||
|
||||
ALTER SCHEMA graphql_public OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: pg_net; Type: EXTENSION; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pg_net WITH SCHEMA extensions;
|
||||
|
||||
|
||||
--
|
||||
-- Name: EXTENSION pg_net; Type: COMMENT; Schema: -; Owner:
|
||||
--
|
||||
|
||||
COMMENT ON EXTENSION pg_net IS 'Async HTTP';
|
||||
|
||||
|
||||
--
|
||||
-- Name: pgbouncer; Type: SCHEMA; Schema: -; Owner: pgbouncer
|
||||
--
|
||||
|
||||
CREATE SCHEMA pgbouncer;
|
||||
|
||||
|
||||
ALTER SCHEMA pgbouncer OWNER TO pgbouncer;
|
||||
|
||||
--
|
||||
-- Name: realtime; Type: SCHEMA; Schema: -; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE SCHEMA realtime;
|
||||
|
||||
|
||||
ALTER SCHEMA realtime OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: storage; Type: SCHEMA; Schema: -; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE SCHEMA storage;
|
||||
|
||||
|
||||
ALTER SCHEMA storage OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: supabase_functions; Type: SCHEMA; Schema: -; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE SCHEMA supabase_functions;
|
||||
|
||||
|
||||
ALTER SCHEMA supabase_functions OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: vault; Type: SCHEMA; Schema: -; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE SCHEMA vault;
|
||||
|
||||
|
||||
ALTER SCHEMA vault OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: btree_gist; Type: EXTENSION; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS btree_gist WITH SCHEMA public;
|
||||
|
||||
|
||||
--
|
||||
-- Name: EXTENSION btree_gist; Type: COMMENT; Schema: -; Owner:
|
||||
--
|
||||
|
||||
COMMENT ON EXTENSION btree_gist IS 'support for indexing common datatypes in GiST';
|
||||
|
||||
|
||||
--
|
||||
-- Name: citext; Type: EXTENSION; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public;
|
||||
|
||||
|
||||
--
|
||||
-- Name: EXTENSION citext; Type: COMMENT; Schema: -; Owner:
|
||||
--
|
||||
|
||||
COMMENT ON EXTENSION citext IS 'data type for case-insensitive character strings';
|
||||
|
||||
|
||||
--
|
||||
-- Name: pg_graphql; Type: EXTENSION; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pg_graphql WITH SCHEMA graphql;
|
||||
|
||||
|
||||
--
|
||||
-- Name: EXTENSION pg_graphql; Type: COMMENT; Schema: -; Owner:
|
||||
--
|
||||
|
||||
COMMENT ON EXTENSION pg_graphql IS 'pg_graphql: GraphQL support';
|
||||
|
||||
|
||||
--
|
||||
-- Name: pg_stat_statements; Type: EXTENSION; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pg_stat_statements WITH SCHEMA extensions;
|
||||
|
||||
|
||||
--
|
||||
-- Name: EXTENSION pg_stat_statements; Type: COMMENT; Schema: -; Owner:
|
||||
--
|
||||
|
||||
COMMENT ON EXTENSION pg_stat_statements IS 'track planning and execution statistics of all SQL statements executed';
|
||||
|
||||
|
||||
--
|
||||
-- Name: pg_trgm; Type: EXTENSION; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;
|
||||
|
||||
|
||||
--
|
||||
-- Name: EXTENSION pg_trgm; Type: COMMENT; Schema: -; Owner:
|
||||
--
|
||||
|
||||
COMMENT ON EXTENSION pg_trgm IS 'text similarity measurement and index searching based on trigrams';
|
||||
|
||||
|
||||
--
|
||||
-- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA extensions;
|
||||
|
||||
|
||||
--
|
||||
-- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner:
|
||||
--
|
||||
|
||||
COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions';
|
||||
|
||||
|
||||
--
|
||||
-- Name: supabase_vault; Type: EXTENSION; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS supabase_vault WITH SCHEMA vault;
|
||||
|
||||
|
||||
--
|
||||
-- Name: EXTENSION supabase_vault; Type: COMMENT; Schema: -; Owner:
|
||||
--
|
||||
|
||||
COMMENT ON EXTENSION supabase_vault IS 'Supabase Vault Extension';
|
||||
|
||||
|
||||
--
|
||||
-- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA extensions;
|
||||
|
||||
|
||||
--
|
||||
-- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner:
|
||||
--
|
||||
|
||||
COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)';
|
||||
|
||||
|
||||
123
database-novo/schema/02_types/auth_types.sql
Normal file
123
database-novo/schema/02_types/auth_types.sql
Normal file
@@ -0,0 +1,123 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Types (Enums) — auth schema
|
||||
-- =============================================================================
|
||||
|
||||
--
|
||||
-- Name: aal_level; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TYPE auth.aal_level AS ENUM (
|
||||
'aal1',
|
||||
'aal2',
|
||||
'aal3'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE auth.aal_level OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: code_challenge_method; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TYPE auth.code_challenge_method AS ENUM (
|
||||
's256',
|
||||
'plain'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE auth.code_challenge_method OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: factor_status; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TYPE auth.factor_status AS ENUM (
|
||||
'unverified',
|
||||
'verified'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE auth.factor_status OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: factor_type; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TYPE auth.factor_type AS ENUM (
|
||||
'totp',
|
||||
'webauthn',
|
||||
'phone'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE auth.factor_type OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: oauth_authorization_status; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TYPE auth.oauth_authorization_status AS ENUM (
|
||||
'pending',
|
||||
'approved',
|
||||
'denied',
|
||||
'expired'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE auth.oauth_authorization_status OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: oauth_client_type; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TYPE auth.oauth_client_type AS ENUM (
|
||||
'public',
|
||||
'confidential'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE auth.oauth_client_type OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: oauth_registration_type; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TYPE auth.oauth_registration_type AS ENUM (
|
||||
'dynamic',
|
||||
'manual'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE auth.oauth_registration_type OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: oauth_response_type; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TYPE auth.oauth_response_type AS ENUM (
|
||||
'code'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE auth.oauth_response_type OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: one_time_token_type; Type: TYPE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TYPE auth.one_time_token_type AS ENUM (
|
||||
'confirmation_token',
|
||||
'reauthentication_token',
|
||||
'recovery_token',
|
||||
'email_change_token_new',
|
||||
'email_change_token_current',
|
||||
'phone_change_token'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE auth.one_time_token_type OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: commitment_log_source; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
88
database-novo/schema/02_types/infra_types.sql
Normal file
88
database-novo/schema/02_types/infra_types.sql
Normal file
@@ -0,0 +1,88 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Types — realtime + storage schemas
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TYPE realtime.action AS ENUM (
|
||||
'INSERT',
|
||||
'UPDATE',
|
||||
'DELETE',
|
||||
'TRUNCATE',
|
||||
'ERROR'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE realtime.action OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: equality_op; Type: TYPE; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TYPE realtime.equality_op AS ENUM (
|
||||
'eq',
|
||||
'neq',
|
||||
'lt',
|
||||
'lte',
|
||||
'gt',
|
||||
'gte',
|
||||
'in'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE realtime.equality_op OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: user_defined_filter; Type: TYPE; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TYPE realtime.user_defined_filter AS (
|
||||
column_name text,
|
||||
op realtime.equality_op,
|
||||
value text
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE realtime.user_defined_filter OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: wal_column; Type: TYPE; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TYPE realtime.wal_column AS (
|
||||
name text,
|
||||
type_name text,
|
||||
type_oid oid,
|
||||
value jsonb,
|
||||
is_pkey boolean,
|
||||
is_selectable boolean
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE realtime.wal_column OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: wal_rls; Type: TYPE; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TYPE realtime.wal_rls AS (
|
||||
wal jsonb,
|
||||
is_rls_enabled boolean,
|
||||
subscription_ids uuid[],
|
||||
errors text[]
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE realtime.wal_rls OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: buckettype; Type: TYPE; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
CREATE TYPE storage.buckettype AS ENUM (
|
||||
'STANDARD',
|
||||
'ANALYTICS',
|
||||
'VECTOR'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE storage.buckettype OWNER TO supabase_storage_admin;
|
||||
|
||||
137
database-novo/schema/02_types/public_types.sql
Normal file
137
database-novo/schema/02_types/public_types.sql
Normal file
@@ -0,0 +1,137 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Types (Enums) — public schema
|
||||
-- =============================================================================
|
||||
-- commitment_log_source, determined_field_type, financial_record_type,
|
||||
-- recurrence_exception_type, recurrence_type, status_agenda_serie,
|
||||
-- status_evento_agenda, status_excecao_agenda, tipo_evento_agenda,
|
||||
-- tipo_excecao_agenda
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TYPE public.commitment_log_source AS ENUM (
|
||||
'manual',
|
||||
'auto'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE public.commitment_log_source OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: determined_field_type; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TYPE public.determined_field_type AS ENUM (
|
||||
'text',
|
||||
'textarea',
|
||||
'number',
|
||||
'date',
|
||||
'select',
|
||||
'boolean'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE public.determined_field_type OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: financial_record_type; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TYPE public.financial_record_type AS ENUM (
|
||||
'receita',
|
||||
'despesa'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE public.financial_record_type OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: recurrence_exception_type; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TYPE public.recurrence_exception_type AS ENUM (
|
||||
'cancel_session',
|
||||
'reschedule_session',
|
||||
'patient_missed',
|
||||
'therapist_canceled',
|
||||
'holiday_block'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE public.recurrence_exception_type OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: recurrence_type; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TYPE public.recurrence_type AS ENUM (
|
||||
'weekly',
|
||||
'biweekly',
|
||||
'monthly',
|
||||
'yearly',
|
||||
'custom_weekdays'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE public.recurrence_type OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: status_agenda_serie; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TYPE public.status_agenda_serie AS ENUM (
|
||||
'ativo',
|
||||
'pausado',
|
||||
'cancelado'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE public.status_agenda_serie OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: status_evento_agenda; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TYPE public.status_evento_agenda AS ENUM (
|
||||
'agendado',
|
||||
'realizado',
|
||||
'faltou',
|
||||
'cancelado',
|
||||
'remarcar'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE public.status_evento_agenda OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: status_excecao_agenda; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TYPE public.status_excecao_agenda AS ENUM (
|
||||
'pendente',
|
||||
'ativo',
|
||||
'arquivado'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE public.status_excecao_agenda OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: tipo_evento_agenda; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TYPE public.tipo_evento_agenda AS ENUM (
|
||||
'sessao',
|
||||
'bloqueio'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE public.tipo_evento_agenda OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: tipo_excecao_agenda; Type: TYPE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TYPE public.tipo_excecao_agenda AS ENUM (
|
||||
'bloqueio',
|
||||
'horario_extra'
|
||||
);
|
||||
|
||||
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
|
||||
--
|
||||
|
||||
472
database-novo/schema/04_tables/agenda.sql
Normal file
472
database-novo/schema/04_tables/agenda.sql
Normal file
@@ -0,0 +1,472 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Tables — Agenda + Recorrências + Agendador Online
|
||||
-- =============================================================================
|
||||
-- agenda_bloqueios, agenda_configuracoes, agenda_eventos, agenda_excecoes,
|
||||
-- agenda_online_slots, agenda_regras_semanais, agenda_slots_bloqueados_semanais,
|
||||
-- agenda_slots_regras, recurrence_rules, recurrence_exceptions,
|
||||
-- recurrence_rule_services, agendador_configuracoes, agendador_solicitacoes
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE public.agenda_bloqueios (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
tenant_id uuid,
|
||||
tipo text NOT NULL,
|
||||
titulo text NOT NULL,
|
||||
data_inicio date NOT NULL,
|
||||
data_fim date,
|
||||
hora_inicio time without time zone,
|
||||
hora_fim time without time zone,
|
||||
recorrente boolean DEFAULT false NOT NULL,
|
||||
dia_semana smallint,
|
||||
observacao text,
|
||||
origem text DEFAULT 'manual'::text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT agenda_bloqueios_tipo_check CHECK ((tipo = ANY (ARRAY['feriado_nacional'::text, 'feriado_municipal'::text, 'bloqueio'::text])))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.agenda_bloqueios OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: agenda_configuracoes; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.agenda_configuracoes (
|
||||
owner_id uuid NOT NULL,
|
||||
duracao_padrao_minutos integer DEFAULT 50 NOT NULL,
|
||||
intervalo_padrao_minutos integer DEFAULT 0 NOT NULL,
|
||||
timezone text DEFAULT 'America/Sao_Paulo'::text NOT NULL,
|
||||
usar_horario_admin_custom boolean DEFAULT false NOT NULL,
|
||||
admin_inicio_visualizacao time without time zone,
|
||||
admin_fim_visualizacao time without time zone,
|
||||
admin_slot_visual_minutos integer DEFAULT 30 NOT NULL,
|
||||
online_ativo boolean DEFAULT false NOT NULL,
|
||||
online_min_antecedencia_horas integer DEFAULT 24 NOT NULL,
|
||||
online_max_dias_futuro integer DEFAULT 60 NOT NULL,
|
||||
online_cancelar_ate_horas integer DEFAULT 12 NOT NULL,
|
||||
online_reagendar_ate_horas integer DEFAULT 12 NOT NULL,
|
||||
online_limite_agendamentos_futuros integer DEFAULT 1 NOT NULL,
|
||||
online_modo text DEFAULT 'automatico'::text NOT NULL,
|
||||
online_buffer_antes_min integer DEFAULT 0 NOT NULL,
|
||||
online_buffer_depois_min integer DEFAULT 0 NOT NULL,
|
||||
online_modalidade text DEFAULT 'ambos'::text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
usar_granularidade_custom boolean DEFAULT false NOT NULL,
|
||||
granularidade_min integer,
|
||||
setup_concluido boolean DEFAULT false NOT NULL,
|
||||
setup_concluido_em timestamp with time zone,
|
||||
agenda_view_mode text DEFAULT 'full_24h'::text NOT NULL,
|
||||
agenda_custom_start time without time zone,
|
||||
agenda_custom_end time without time zone,
|
||||
session_duration_min integer DEFAULT 50 NOT NULL,
|
||||
session_break_min integer DEFAULT 10 NOT NULL,
|
||||
pausas_semanais jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
setup_clinica_concluido boolean DEFAULT false NOT NULL,
|
||||
setup_clinica_concluido_em timestamp with time zone,
|
||||
tenant_id uuid,
|
||||
jornada_igual_todos boolean DEFAULT true,
|
||||
slot_mode text DEFAULT 'fixed'::text NOT NULL,
|
||||
CONSTRAINT agenda_configuracoes_admin_slot_visual_minutos_check CHECK ((admin_slot_visual_minutos = ANY (ARRAY[5, 10, 15, 20, 30, 60]))),
|
||||
CONSTRAINT agenda_configuracoes_check CHECK (((usar_horario_admin_custom = false) OR ((admin_inicio_visualizacao IS NOT NULL) AND (admin_fim_visualizacao IS NOT NULL) AND (admin_fim_visualizacao > admin_inicio_visualizacao)))),
|
||||
CONSTRAINT agenda_configuracoes_duracao_padrao_minutos_check CHECK (((duracao_padrao_minutos >= 10) AND (duracao_padrao_minutos <= 240))),
|
||||
CONSTRAINT agenda_configuracoes_granularidade_min_check CHECK (((granularidade_min IS NULL) OR (granularidade_min = ANY (ARRAY[5, 10, 15, 20, 30, 45, 50, 60])))),
|
||||
CONSTRAINT agenda_configuracoes_intervalo_padrao_minutos_check CHECK (((intervalo_padrao_minutos >= 0) AND (intervalo_padrao_minutos <= 120))),
|
||||
CONSTRAINT agenda_configuracoes_online_buffer_antes_min_check CHECK (((online_buffer_antes_min >= 0) AND (online_buffer_antes_min <= 120))),
|
||||
CONSTRAINT agenda_configuracoes_online_buffer_depois_min_check CHECK (((online_buffer_depois_min >= 0) AND (online_buffer_depois_min <= 120))),
|
||||
CONSTRAINT agenda_configuracoes_online_cancelar_ate_horas_check CHECK (((online_cancelar_ate_horas >= 0) AND (online_cancelar_ate_horas <= 720))),
|
||||
CONSTRAINT agenda_configuracoes_online_limite_agendamentos_futuros_check CHECK (((online_limite_agendamentos_futuros >= 1) AND (online_limite_agendamentos_futuros <= 10))),
|
||||
CONSTRAINT agenda_configuracoes_online_max_dias_futuro_check CHECK (((online_max_dias_futuro >= 1) AND (online_max_dias_futuro <= 365))),
|
||||
CONSTRAINT agenda_configuracoes_online_min_antecedencia_horas_check CHECK (((online_min_antecedencia_horas >= 0) AND (online_min_antecedencia_horas <= 720))),
|
||||
CONSTRAINT agenda_configuracoes_online_modalidade_check CHECK ((online_modalidade = ANY (ARRAY['online'::text, 'presencial'::text, 'ambos'::text]))),
|
||||
CONSTRAINT agenda_configuracoes_online_modo_check CHECK ((online_modo = ANY (ARRAY['automatico'::text, 'aprovacao'::text]))),
|
||||
CONSTRAINT agenda_configuracoes_online_reagendar_ate_horas_check CHECK (((online_reagendar_ate_horas >= 0) AND (online_reagendar_ate_horas <= 720))),
|
||||
CONSTRAINT agenda_configuracoes_slot_mode_chk CHECK ((slot_mode = ANY (ARRAY['fixed'::text, 'dynamic'::text]))),
|
||||
CONSTRAINT session_break_min_chk CHECK (((session_break_min >= 0) AND (session_break_min <= 60))),
|
||||
CONSTRAINT session_duration_min_chk CHECK (((session_duration_min >= 10) AND (session_duration_min <= 240)))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.agenda_configuracoes OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: agenda_eventos; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.agenda_eventos (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
tipo public.tipo_evento_agenda DEFAULT 'sessao'::public.tipo_evento_agenda NOT NULL,
|
||||
status public.status_evento_agenda DEFAULT 'agendado'::public.status_evento_agenda NOT NULL,
|
||||
titulo text,
|
||||
observacoes text,
|
||||
inicio_em timestamp with time zone NOT NULL,
|
||||
fim_em timestamp with time zone NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
terapeuta_id uuid,
|
||||
tenant_id uuid NOT NULL,
|
||||
visibility_scope text DEFAULT 'public'::text NOT NULL,
|
||||
mirror_of_event_id uuid,
|
||||
mirror_source text,
|
||||
patient_id uuid,
|
||||
determined_commitment_id uuid,
|
||||
link_online text,
|
||||
titulo_custom text,
|
||||
extra_fields jsonb,
|
||||
recurrence_id uuid,
|
||||
recurrence_date date,
|
||||
modalidade text DEFAULT 'presencial'::text,
|
||||
price numeric(10,2),
|
||||
billing_contract_id uuid,
|
||||
billed boolean DEFAULT false NOT NULL,
|
||||
services_customized boolean DEFAULT false NOT NULL,
|
||||
insurance_plan_id uuid,
|
||||
insurance_guide_number text,
|
||||
insurance_value numeric(10,2),
|
||||
insurance_plan_service_id uuid,
|
||||
CONSTRAINT agenda_eventos_check CHECK ((fim_em > inicio_em))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.agenda_eventos OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: COLUMN agenda_eventos.price; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN public.agenda_eventos.price IS 'Valor da sessão em BRL. Preenchido automaticamente pela tabela professional_pricing do profissional.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_excecoes; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.agenda_excecoes (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
data date NOT NULL,
|
||||
hora_inicio time without time zone,
|
||||
hora_fim time without time zone,
|
||||
tipo public.tipo_excecao_agenda NOT NULL,
|
||||
motivo text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
status public.status_excecao_agenda DEFAULT 'ativo'::public.status_excecao_agenda NOT NULL,
|
||||
fonte text DEFAULT 'manual'::text NOT NULL,
|
||||
aplicavel_online boolean DEFAULT true NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
CONSTRAINT agenda_excecoes_check CHECK ((((hora_inicio IS NULL) AND (hora_fim IS NULL)) OR ((hora_inicio IS NOT NULL) AND (hora_fim IS NOT NULL) AND (hora_fim > hora_inicio)))),
|
||||
CONSTRAINT agenda_excecoes_fonte_check CHECK ((fonte = ANY (ARRAY['manual'::text, 'feriado_google'::text, 'sistema'::text])))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.agenda_excecoes OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: agenda_online_slots; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.agenda_online_slots (
|
||||
id bigint NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
weekday integer NOT NULL,
|
||||
"time" time without time zone NOT NULL,
|
||||
enabled boolean DEFAULT true NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
CONSTRAINT agenda_online_slots_weekday_check CHECK ((weekday = ANY (ARRAY[0, 1, 2, 3, 4, 5, 6])))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.agenda_online_slots OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: agenda_online_slots_id_seq; Type: SEQUENCE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.agenda_online_slots_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE public.agenda_online_slots_id_seq OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: agenda_online_slots_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.agenda_online_slots_id_seq OWNED BY public.agenda_online_slots.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_regras_semanais; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.agenda_regras_semanais (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
dia_semana smallint NOT NULL,
|
||||
hora_inicio time without time zone NOT NULL,
|
||||
hora_fim time without time zone NOT NULL,
|
||||
modalidade text DEFAULT 'ambos'::text NOT NULL,
|
||||
ativo boolean DEFAULT true NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
CONSTRAINT agenda_regras_semanais_check CHECK ((hora_fim > hora_inicio)),
|
||||
CONSTRAINT agenda_regras_semanais_dia_semana_check CHECK (((dia_semana >= 0) AND (dia_semana <= 6))),
|
||||
CONSTRAINT agenda_regras_semanais_modalidade_check CHECK (((modalidade = ANY (ARRAY['online'::text, 'presencial'::text, 'ambos'::text])) OR (modalidade IS NULL)))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.agenda_regras_semanais OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: agenda_slots_bloqueados_semanais; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.agenda_slots_bloqueados_semanais (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
dia_semana smallint NOT NULL,
|
||||
hora_inicio time without time zone NOT NULL,
|
||||
motivo text,
|
||||
ativo boolean DEFAULT true NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
CONSTRAINT agenda_slots_bloqueados_semanais_dia_semana_check CHECK (((dia_semana >= 0) AND (dia_semana <= 6)))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.agenda_slots_bloqueados_semanais OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: agenda_slots_regras; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.agenda_slots_regras (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
dia_semana smallint NOT NULL,
|
||||
passo_minutos integer NOT NULL,
|
||||
offset_minutos integer DEFAULT 0 NOT NULL,
|
||||
buffer_antes_min integer DEFAULT 0 NOT NULL,
|
||||
buffer_depois_min integer DEFAULT 0 NOT NULL,
|
||||
min_antecedencia_horas integer DEFAULT 0 NOT NULL,
|
||||
ativo boolean DEFAULT true NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
CONSTRAINT agenda_slots_regras_buffer_antes_min_check CHECK (((buffer_antes_min >= 0) AND (buffer_antes_min <= 240))),
|
||||
CONSTRAINT agenda_slots_regras_buffer_depois_min_check CHECK (((buffer_depois_min >= 0) AND (buffer_depois_min <= 240))),
|
||||
CONSTRAINT agenda_slots_regras_dia_semana_check CHECK (((dia_semana >= 0) AND (dia_semana <= 6))),
|
||||
CONSTRAINT agenda_slots_regras_min_antecedencia_horas_check CHECK (((min_antecedencia_horas >= 0) AND (min_antecedencia_horas <= 720))),
|
||||
CONSTRAINT agenda_slots_regras_offset_minutos_check CHECK (((offset_minutos >= 0) AND (offset_minutos <= 55))),
|
||||
CONSTRAINT agenda_slots_regras_passo_minutos_check CHECK (((passo_minutos >= 5) AND (passo_minutos <= 240)))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.agenda_slots_regras OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: agendador_configuracoes; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
|
||||
CREATE TABLE public.agendador_configuracoes (
|
||||
owner_id uuid NOT NULL,
|
||||
tenant_id uuid,
|
||||
ativo boolean DEFAULT false NOT NULL,
|
||||
link_slug text,
|
||||
imagem_fundo_url text,
|
||||
imagem_header_url text,
|
||||
logomarca_url text,
|
||||
cor_primaria text DEFAULT '#4b6bff'::text,
|
||||
nome_exibicao text,
|
||||
endereco text,
|
||||
botao_como_chegar_ativo boolean DEFAULT true NOT NULL,
|
||||
maps_url text,
|
||||
modo_aprovacao text DEFAULT 'aprovacao'::text NOT NULL,
|
||||
modalidade text DEFAULT 'presencial'::text NOT NULL,
|
||||
tipos_habilitados jsonb DEFAULT '["primeira", "retorno"]'::jsonb NOT NULL,
|
||||
duracao_sessao_min integer DEFAULT 50 NOT NULL,
|
||||
antecedencia_minima_horas integer DEFAULT 24 NOT NULL,
|
||||
prazo_resposta_horas integer DEFAULT 2 NOT NULL,
|
||||
reserva_horas integer DEFAULT 2 NOT NULL,
|
||||
pagamento_obrigatorio boolean DEFAULT false NOT NULL,
|
||||
pix_chave text,
|
||||
pix_countdown_minutos integer DEFAULT 20 NOT NULL,
|
||||
triagem_motivo boolean DEFAULT true NOT NULL,
|
||||
triagem_como_conheceu boolean DEFAULT false NOT NULL,
|
||||
verificacao_email boolean DEFAULT false NOT NULL,
|
||||
exigir_aceite_lgpd boolean DEFAULT true NOT NULL,
|
||||
mensagem_boas_vindas text,
|
||||
texto_como_se_preparar text,
|
||||
texto_termos_lgpd text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
pagamento_modo text DEFAULT 'sem_pagamento'::text NOT NULL,
|
||||
pagamento_metodos_visiveis text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
CONSTRAINT agendador_configuracoes_antecedencia_check CHECK (((antecedencia_minima_horas >= 0) AND (antecedencia_minima_horas <= 720))),
|
||||
CONSTRAINT agendador_configuracoes_duracao_check CHECK (((duracao_sessao_min >= 10) AND (duracao_sessao_min <= 240))),
|
||||
CONSTRAINT agendador_configuracoes_modalidade_check CHECK ((modalidade = ANY (ARRAY['presencial'::text, 'online'::text, 'ambos'::text]))),
|
||||
CONSTRAINT agendador_configuracoes_modo_check CHECK ((modo_aprovacao = ANY (ARRAY['automatico'::text, 'aprovacao'::text]))),
|
||||
CONSTRAINT agendador_configuracoes_pix_countdown_check CHECK (((pix_countdown_minutos >= 5) AND (pix_countdown_minutos <= 120))),
|
||||
CONSTRAINT agendador_configuracoes_prazo_check CHECK (((prazo_resposta_horas >= 1) AND (prazo_resposta_horas <= 72))),
|
||||
CONSTRAINT agendador_configuracoes_reserva_check CHECK (((reserva_horas >= 1) AND (reserva_horas <= 48)))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.agendador_configuracoes OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: COLUMN agendador_configuracoes.pagamento_modo; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN public.agendador_configuracoes.pagamento_modo IS 'sem_pagamento | pagar_na_hora | pix_antecipado';
|
||||
|
||||
|
||||
--
|
||||
-- Name: COLUMN agendador_configuracoes.pagamento_metodos_visiveis; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN public.agendador_configuracoes.pagamento_metodos_visiveis IS 'Métodos exibidos ao paciente quando pagamento_modo = pagar_na_hora. Ex: {pix, deposito, dinheiro, cartao, convenio}';
|
||||
|
||||
|
||||
--
|
||||
-- Name: agendador_solicitacoes; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.agendador_solicitacoes (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
tenant_id uuid,
|
||||
paciente_nome text NOT NULL,
|
||||
paciente_sobrenome text,
|
||||
paciente_email text NOT NULL,
|
||||
paciente_celular text,
|
||||
paciente_cpf text,
|
||||
tipo text NOT NULL,
|
||||
modalidade text NOT NULL,
|
||||
data_solicitada date NOT NULL,
|
||||
hora_solicitada time without time zone NOT NULL,
|
||||
reservado_ate timestamp with time zone,
|
||||
motivo text,
|
||||
como_conheceu text,
|
||||
pix_status text DEFAULT 'pendente'::text,
|
||||
pix_pago_em timestamp with time zone,
|
||||
status text DEFAULT 'pendente'::text NOT NULL,
|
||||
recusado_motivo text,
|
||||
autorizado_em timestamp with time zone,
|
||||
autorizado_por uuid,
|
||||
user_id uuid,
|
||||
patient_id uuid,
|
||||
evento_id uuid,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT agendador_sol_modalidade_check CHECK ((modalidade = ANY (ARRAY['presencial'::text, 'online'::text]))),
|
||||
CONSTRAINT agendador_sol_pix_check CHECK (((pix_status IS NULL) OR (pix_status = ANY (ARRAY['pendente'::text, 'pago'::text, 'expirado'::text])))),
|
||||
CONSTRAINT agendador_sol_status_check CHECK ((status = ANY (ARRAY['pendente'::text, 'autorizado'::text, 'recusado'::text, 'expirado'::text, 'convertido'::text]))),
|
||||
CONSTRAINT agendador_sol_tipo_check CHECK ((tipo = ANY (ARRAY['primeira'::text, 'retorno'::text, 'reagendar'::text])))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.agendador_solicitacoes OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: billing_contracts; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
|
||||
CREATE TABLE public.recurrence_exceptions (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
recurrence_id uuid NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
original_date date NOT NULL,
|
||||
type public.recurrence_exception_type NOT NULL,
|
||||
new_date date,
|
||||
new_start_time time without time zone,
|
||||
new_end_time time without time zone,
|
||||
modalidade text,
|
||||
observacoes text,
|
||||
titulo_custom text,
|
||||
extra_fields jsonb,
|
||||
reason text,
|
||||
agenda_evento_id uuid,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.recurrence_exceptions OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: recurrence_rule_services; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.recurrence_rule_services (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
rule_id uuid NOT NULL,
|
||||
service_id uuid NOT NULL,
|
||||
quantity integer DEFAULT 1 NOT NULL,
|
||||
unit_price numeric(10,2) NOT NULL,
|
||||
discount_pct numeric(5,2) DEFAULT 0,
|
||||
discount_flat numeric(10,2) DEFAULT 0,
|
||||
final_price numeric(10,2) NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
CONSTRAINT recurrence_rule_services_disc_flat_chk CHECK ((discount_flat >= (0)::numeric)),
|
||||
CONSTRAINT recurrence_rule_services_disc_pct_chk CHECK (((discount_pct >= (0)::numeric) AND (discount_pct <= (100)::numeric))),
|
||||
CONSTRAINT recurrence_rule_services_final_price_chk CHECK ((final_price >= (0)::numeric)),
|
||||
CONSTRAINT recurrence_rule_services_quantity_chk CHECK ((quantity > 0))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.recurrence_rule_services OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: recurrence_rules; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.recurrence_rules (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
therapist_id uuid,
|
||||
patient_id uuid,
|
||||
determined_commitment_id uuid,
|
||||
type public.recurrence_type DEFAULT 'weekly'::public.recurrence_type NOT NULL,
|
||||
"interval" smallint DEFAULT 1 NOT NULL,
|
||||
weekdays smallint[] DEFAULT '{}'::smallint[] NOT NULL,
|
||||
start_time time without time zone NOT NULL,
|
||||
end_time time without time zone NOT NULL,
|
||||
timezone text DEFAULT 'America/Sao_Paulo'::text NOT NULL,
|
||||
duration_min smallint DEFAULT 50 NOT NULL,
|
||||
start_date date NOT NULL,
|
||||
end_date date,
|
||||
max_occurrences integer,
|
||||
open_ended boolean DEFAULT true NOT NULL,
|
||||
modalidade text DEFAULT 'presencial'::text,
|
||||
titulo_custom text,
|
||||
observacoes text,
|
||||
extra_fields jsonb,
|
||||
status text DEFAULT 'ativo'::text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
price numeric(10,2),
|
||||
insurance_plan_id uuid,
|
||||
insurance_guide_number text,
|
||||
insurance_value numeric(10,2),
|
||||
insurance_plan_service_id uuid,
|
||||
CONSTRAINT recurrence_rules_dates_chk CHECK (((end_date IS NULL) OR (end_date >= start_date))),
|
||||
CONSTRAINT recurrence_rules_interval_chk CHECK (("interval" >= 1)),
|
||||
CONSTRAINT recurrence_rules_status_check CHECK ((status = ANY (ARRAY['ativo'::text, 'pausado'::text, 'cancelado'::text]))),
|
||||
CONSTRAINT recurrence_rules_times_chk CHECK ((end_time > start_time))
|
||||
);
|
||||
|
||||
|
||||
608
database-novo/schema/04_tables/auth.sql
Normal file
608
database-novo/schema/04_tables/auth.sql
Normal file
@@ -0,0 +1,608 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Tables — auth schema (Supabase GoTrue)
|
||||
-- =============================================================================
|
||||
-- auth.users, auth.identities, auth.sessions, auth.refresh_tokens,
|
||||
-- auth.mfa_*, auth.saml_*, auth.sso_*, auth.flow_state, etc.
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE auth.audit_log_entries (
|
||||
instance_id uuid,
|
||||
id uuid NOT NULL,
|
||||
payload json,
|
||||
created_at timestamp with time zone,
|
||||
ip_address character varying(64) DEFAULT ''::character varying NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.audit_log_entries OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE audit_log_entries; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.audit_log_entries IS 'Auth: Audit trail for user actions.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: flow_state; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.flow_state (
|
||||
id uuid NOT NULL,
|
||||
user_id uuid,
|
||||
auth_code text,
|
||||
code_challenge_method auth.code_challenge_method,
|
||||
code_challenge text,
|
||||
provider_type text NOT NULL,
|
||||
provider_access_token text,
|
||||
provider_refresh_token text,
|
||||
created_at timestamp with time zone,
|
||||
updated_at timestamp with time zone,
|
||||
authentication_method text NOT NULL,
|
||||
auth_code_issued_at timestamp with time zone,
|
||||
invite_token text,
|
||||
referrer text,
|
||||
oauth_client_state_id uuid,
|
||||
linking_target_id uuid,
|
||||
email_optional boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.flow_state OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE flow_state; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.flow_state IS 'Stores metadata for all OAuth/SSO login flows';
|
||||
|
||||
|
||||
--
|
||||
-- Name: identities; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.identities (
|
||||
provider_id text NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
identity_data jsonb NOT NULL,
|
||||
provider text NOT NULL,
|
||||
last_sign_in_at timestamp with time zone,
|
||||
created_at timestamp with time zone,
|
||||
updated_at timestamp with time zone,
|
||||
email text GENERATED ALWAYS AS (lower((identity_data ->> 'email'::text))) STORED,
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.identities OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE identities; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.identities IS 'Auth: Stores identities associated to a user.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: COLUMN identities.email; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN auth.identities.email IS 'Auth: Email is a generated column that references the optional email property in the identity_data';
|
||||
|
||||
|
||||
--
|
||||
-- Name: instances; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.instances (
|
||||
id uuid NOT NULL,
|
||||
uuid uuid,
|
||||
raw_base_config text,
|
||||
created_at timestamp with time zone,
|
||||
updated_at timestamp with time zone
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.instances OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE instances; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.instances IS 'Auth: Manages users across multiple sites.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: mfa_amr_claims; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.mfa_amr_claims (
|
||||
session_id uuid NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
authentication_method text NOT NULL,
|
||||
id uuid NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.mfa_amr_claims OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE mfa_amr_claims; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.mfa_amr_claims IS 'auth: stores authenticator method reference claims for multi factor authentication';
|
||||
|
||||
|
||||
--
|
||||
-- Name: mfa_challenges; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.mfa_challenges (
|
||||
id uuid NOT NULL,
|
||||
factor_id uuid NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
verified_at timestamp with time zone,
|
||||
ip_address inet NOT NULL,
|
||||
otp_code text,
|
||||
web_authn_session_data jsonb
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.mfa_challenges OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE mfa_challenges; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.mfa_challenges IS 'auth: stores metadata about challenge requests made';
|
||||
|
||||
|
||||
--
|
||||
-- Name: mfa_factors; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.mfa_factors (
|
||||
id uuid NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
friendly_name text,
|
||||
factor_type auth.factor_type NOT NULL,
|
||||
status auth.factor_status NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
secret text,
|
||||
phone text,
|
||||
last_challenged_at timestamp with time zone,
|
||||
web_authn_credential jsonb,
|
||||
web_authn_aaguid uuid,
|
||||
last_webauthn_challenge_data jsonb
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.mfa_factors OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE mfa_factors; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.mfa_factors IS 'auth: stores metadata about factors';
|
||||
|
||||
|
||||
--
|
||||
-- Name: COLUMN mfa_factors.last_webauthn_challenge_data; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN auth.mfa_factors.last_webauthn_challenge_data IS 'Stores the latest WebAuthn challenge data including attestation/assertion for customer verification';
|
||||
|
||||
|
||||
--
|
||||
-- Name: oauth_authorizations; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.oauth_authorizations (
|
||||
id uuid NOT NULL,
|
||||
authorization_id text NOT NULL,
|
||||
client_id uuid NOT NULL,
|
||||
user_id uuid,
|
||||
redirect_uri text NOT NULL,
|
||||
scope text NOT NULL,
|
||||
state text,
|
||||
resource text,
|
||||
code_challenge text,
|
||||
code_challenge_method auth.code_challenge_method,
|
||||
response_type auth.oauth_response_type DEFAULT 'code'::auth.oauth_response_type NOT NULL,
|
||||
status auth.oauth_authorization_status DEFAULT 'pending'::auth.oauth_authorization_status NOT NULL,
|
||||
authorization_code text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
expires_at timestamp with time zone DEFAULT (now() + '00:03:00'::interval) NOT NULL,
|
||||
approved_at timestamp with time zone,
|
||||
nonce text,
|
||||
CONSTRAINT oauth_authorizations_authorization_code_length CHECK ((char_length(authorization_code) <= 255)),
|
||||
CONSTRAINT oauth_authorizations_code_challenge_length CHECK ((char_length(code_challenge) <= 128)),
|
||||
CONSTRAINT oauth_authorizations_expires_at_future CHECK ((expires_at > created_at)),
|
||||
CONSTRAINT oauth_authorizations_nonce_length CHECK ((char_length(nonce) <= 255)),
|
||||
CONSTRAINT oauth_authorizations_redirect_uri_length CHECK ((char_length(redirect_uri) <= 2048)),
|
||||
CONSTRAINT oauth_authorizations_resource_length CHECK ((char_length(resource) <= 2048)),
|
||||
CONSTRAINT oauth_authorizations_scope_length CHECK ((char_length(scope) <= 4096)),
|
||||
CONSTRAINT oauth_authorizations_state_length CHECK ((char_length(state) <= 4096))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.oauth_authorizations OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: oauth_client_states; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.oauth_client_states (
|
||||
id uuid NOT NULL,
|
||||
provider_type text NOT NULL,
|
||||
code_verifier text,
|
||||
created_at timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.oauth_client_states OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE oauth_client_states; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.oauth_client_states IS 'Stores OAuth states for third-party provider authentication flows where Supabase acts as the OAuth client.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: oauth_clients; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.oauth_clients (
|
||||
id uuid NOT NULL,
|
||||
client_secret_hash text,
|
||||
registration_type auth.oauth_registration_type NOT NULL,
|
||||
redirect_uris text NOT NULL,
|
||||
grant_types text NOT NULL,
|
||||
client_name text,
|
||||
client_uri text,
|
||||
logo_uri 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,
|
||||
client_type auth.oauth_client_type DEFAULT 'confidential'::auth.oauth_client_type NOT NULL,
|
||||
token_endpoint_auth_method text NOT NULL,
|
||||
CONSTRAINT oauth_clients_client_name_length CHECK ((char_length(client_name) <= 1024)),
|
||||
CONSTRAINT oauth_clients_client_uri_length CHECK ((char_length(client_uri) <= 2048)),
|
||||
CONSTRAINT oauth_clients_logo_uri_length CHECK ((char_length(logo_uri) <= 2048)),
|
||||
CONSTRAINT oauth_clients_token_endpoint_auth_method_check CHECK ((token_endpoint_auth_method = ANY (ARRAY['client_secret_basic'::text, 'client_secret_post'::text, 'none'::text])))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.oauth_clients OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: oauth_consents; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.oauth_consents (
|
||||
id uuid NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
client_id uuid NOT NULL,
|
||||
scopes text NOT NULL,
|
||||
granted_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
revoked_at timestamp with time zone,
|
||||
CONSTRAINT oauth_consents_revoked_after_granted CHECK (((revoked_at IS NULL) OR (revoked_at >= granted_at))),
|
||||
CONSTRAINT oauth_consents_scopes_length CHECK ((char_length(scopes) <= 2048)),
|
||||
CONSTRAINT oauth_consents_scopes_not_empty CHECK ((char_length(TRIM(BOTH FROM scopes)) > 0))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.oauth_consents OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: one_time_tokens; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.one_time_tokens (
|
||||
id uuid NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
token_type auth.one_time_token_type NOT NULL,
|
||||
token_hash text NOT NULL,
|
||||
relates_to text NOT NULL,
|
||||
created_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT one_time_tokens_token_hash_check CHECK ((char_length(token_hash) > 0))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.one_time_tokens OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: refresh_tokens; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.refresh_tokens (
|
||||
instance_id uuid,
|
||||
id bigint NOT NULL,
|
||||
token character varying(255),
|
||||
user_id character varying(255),
|
||||
revoked boolean,
|
||||
created_at timestamp with time zone,
|
||||
updated_at timestamp with time zone,
|
||||
parent character varying(255),
|
||||
session_id uuid
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.refresh_tokens OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE refresh_tokens; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.refresh_tokens IS 'Auth: Store of tokens used to refresh JWT tokens once they expire.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: refresh_tokens_id_seq; Type: SEQUENCE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE SEQUENCE auth.refresh_tokens_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE auth.refresh_tokens_id_seq OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: refresh_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
ALTER SEQUENCE auth.refresh_tokens_id_seq OWNED BY auth.refresh_tokens.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: saml_providers; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.saml_providers (
|
||||
id uuid NOT NULL,
|
||||
sso_provider_id uuid NOT NULL,
|
||||
entity_id text NOT NULL,
|
||||
metadata_xml text NOT NULL,
|
||||
metadata_url text,
|
||||
attribute_mapping jsonb,
|
||||
created_at timestamp with time zone,
|
||||
updated_at timestamp with time zone,
|
||||
name_id_format text,
|
||||
CONSTRAINT "entity_id not empty" CHECK ((char_length(entity_id) > 0)),
|
||||
CONSTRAINT "metadata_url not empty" CHECK (((metadata_url = NULL::text) OR (char_length(metadata_url) > 0))),
|
||||
CONSTRAINT "metadata_xml not empty" CHECK ((char_length(metadata_xml) > 0))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.saml_providers OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE saml_providers; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.saml_providers IS 'Auth: Manages SAML Identity Provider connections.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: saml_relay_states; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.saml_relay_states (
|
||||
id uuid NOT NULL,
|
||||
sso_provider_id uuid NOT NULL,
|
||||
request_id text NOT NULL,
|
||||
for_email text,
|
||||
redirect_to text,
|
||||
created_at timestamp with time zone,
|
||||
updated_at timestamp with time zone,
|
||||
flow_state_id uuid,
|
||||
CONSTRAINT "request_id not empty" CHECK ((char_length(request_id) > 0))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.saml_relay_states OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE saml_relay_states; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.saml_relay_states IS 'Auth: Contains SAML Relay State information for each Service Provider initiated login.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: schema_migrations; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.schema_migrations (
|
||||
version character varying(255) NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.schema_migrations OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE schema_migrations; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.schema_migrations IS 'Auth: Manages updates to the auth system.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: sessions; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.sessions (
|
||||
id uuid NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
created_at timestamp with time zone,
|
||||
updated_at timestamp with time zone,
|
||||
factor_id uuid,
|
||||
aal auth.aal_level,
|
||||
not_after timestamp with time zone,
|
||||
refreshed_at timestamp without time zone,
|
||||
user_agent text,
|
||||
ip inet,
|
||||
tag text,
|
||||
oauth_client_id uuid,
|
||||
refresh_token_hmac_key text,
|
||||
refresh_token_counter bigint,
|
||||
scopes text,
|
||||
CONSTRAINT sessions_scopes_length CHECK ((char_length(scopes) <= 4096))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.sessions OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE sessions; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.sessions IS 'Auth: Stores session data associated to a user.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: COLUMN sessions.not_after; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN auth.sessions.not_after IS 'Auth: Not after is a nullable column that contains a timestamp after which the session should be regarded as expired.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: COLUMN sessions.refresh_token_hmac_key; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN auth.sessions.refresh_token_hmac_key IS 'Holds a HMAC-SHA256 key used to sign refresh tokens for this session.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: COLUMN sessions.refresh_token_counter; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN auth.sessions.refresh_token_counter IS 'Holds the ID (counter) of the last issued refresh token.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: sso_domains; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.sso_domains (
|
||||
id uuid NOT NULL,
|
||||
sso_provider_id uuid NOT NULL,
|
||||
domain text NOT NULL,
|
||||
created_at timestamp with time zone,
|
||||
updated_at timestamp with time zone,
|
||||
CONSTRAINT "domain not empty" CHECK ((char_length(domain) > 0))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.sso_domains OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE sso_domains; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.sso_domains IS 'Auth: Manages SSO email address domain mapping to an SSO Identity Provider.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: sso_providers; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.sso_providers (
|
||||
id uuid NOT NULL,
|
||||
resource_id text,
|
||||
created_at timestamp with time zone,
|
||||
updated_at timestamp with time zone,
|
||||
disabled boolean,
|
||||
CONSTRAINT "resource_id not empty" CHECK (((resource_id = NULL::text) OR (char_length(resource_id) > 0)))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.sso_providers OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE sso_providers; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.sso_providers IS 'Auth: Manages SSO identity provider information; see saml_providers for SAML.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: COLUMN sso_providers.resource_id; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN auth.sso_providers.resource_id IS 'Auth: Uniquely identifies a SSO provider according to a user-chosen resource ID (case insensitive), useful in infrastructure as code.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: users; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TABLE auth.users (
|
||||
instance_id uuid,
|
||||
id uuid NOT NULL,
|
||||
aud character varying(255),
|
||||
role character varying(255),
|
||||
email character varying(255),
|
||||
encrypted_password character varying(255),
|
||||
email_confirmed_at timestamp with time zone,
|
||||
invited_at timestamp with time zone,
|
||||
confirmation_token character varying(255),
|
||||
confirmation_sent_at timestamp with time zone,
|
||||
recovery_token character varying(255),
|
||||
recovery_sent_at timestamp with time zone,
|
||||
email_change_token_new character varying(255),
|
||||
email_change character varying(255),
|
||||
email_change_sent_at timestamp with time zone,
|
||||
last_sign_in_at timestamp with time zone,
|
||||
raw_app_meta_data jsonb,
|
||||
raw_user_meta_data jsonb,
|
||||
is_super_admin boolean,
|
||||
created_at timestamp with time zone,
|
||||
updated_at timestamp with time zone,
|
||||
phone text DEFAULT NULL::character varying,
|
||||
phone_confirmed_at timestamp with time zone,
|
||||
phone_change text DEFAULT ''::character varying,
|
||||
phone_change_token character varying(255) DEFAULT ''::character varying,
|
||||
phone_change_sent_at timestamp with time zone,
|
||||
confirmed_at timestamp with time zone GENERATED ALWAYS AS (LEAST(email_confirmed_at, phone_confirmed_at)) STORED,
|
||||
email_change_token_current character varying(255) DEFAULT ''::character varying,
|
||||
email_change_confirm_status smallint DEFAULT 0,
|
||||
banned_until timestamp with time zone,
|
||||
reauthentication_token character varying(255) DEFAULT ''::character varying,
|
||||
reauthentication_sent_at timestamp with time zone,
|
||||
is_sso_user boolean DEFAULT false NOT NULL,
|
||||
deleted_at timestamp with time zone,
|
||||
is_anonymous boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT users_email_change_confirm_status_check CHECK (((email_change_confirm_status >= 0) AND (email_change_confirm_status <= 2)))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE auth.users OWNER TO supabase_auth_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE users; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE auth.users IS 'Auth: Stores user login data within a secure schema.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: COLUMN users.is_sso_user; Type: COMMENT; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN auth.users.is_sso_user IS 'Auth: Set this column to true when the account comes from SSO. These accounts can have duplicate emails.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: addon_credits; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
77
database-novo/schema/04_tables/commitments.sql
Normal file
77
database-novo/schema/04_tables/commitments.sql
Normal file
@@ -0,0 +1,77 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Tables — Compromissos Determinados
|
||||
-- =============================================================================
|
||||
-- determined_commitments, determined_commitment_fields,
|
||||
-- commitment_services, commitment_time_logs
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE public.determined_commitments (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
created_by uuid,
|
||||
is_native boolean DEFAULT false NOT NULL,
|
||||
native_key text,
|
||||
is_locked boolean DEFAULT false NOT NULL,
|
||||
active boolean DEFAULT true NOT NULL,
|
||||
name text NOT NULL,
|
||||
description text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
bg_color text,
|
||||
text_color text
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.determined_commitment_fields (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
commitment_id uuid NOT NULL,
|
||||
key text NOT NULL,
|
||||
label text NOT NULL,
|
||||
field_type public.determined_field_type DEFAULT 'text'::public.determined_field_type NOT NULL,
|
||||
required boolean DEFAULT false NOT NULL,
|
||||
sort_order integer DEFAULT 0 NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.commitment_services (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
commitment_id uuid NOT NULL,
|
||||
service_id uuid NOT NULL,
|
||||
quantity integer DEFAULT 1 NOT NULL,
|
||||
unit_price numeric(10,2) NOT NULL,
|
||||
discount_pct numeric(5,2) DEFAULT 0,
|
||||
discount_flat numeric(10,2) DEFAULT 0,
|
||||
final_price numeric(10,2) NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
CONSTRAINT commitment_services_disc_flat_chk CHECK ((discount_flat >= (0)::numeric)),
|
||||
CONSTRAINT commitment_services_disc_pct_chk CHECK (((discount_pct >= (0)::numeric) AND (discount_pct <= (100)::numeric))),
|
||||
CONSTRAINT commitment_services_final_price_chk CHECK ((final_price >= (0)::numeric)),
|
||||
CONSTRAINT commitment_services_quantity_chk CHECK ((quantity > 0))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.commitment_services OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: commitment_time_logs; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.commitment_time_logs (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
commitment_id uuid NOT NULL,
|
||||
calendar_event_id uuid,
|
||||
source public.commitment_log_source DEFAULT 'manual'::public.commitment_log_source NOT NULL,
|
||||
started_at timestamp with time zone NOT NULL,
|
||||
ended_at timestamp with time zone NOT NULL,
|
||||
minutes integer NOT NULL,
|
||||
created_by uuid,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
171
database-novo/schema/04_tables/core.sql
Normal file
171
database-novo/schema/04_tables/core.sql
Normal file
@@ -0,0 +1,171 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Tables — Core
|
||||
-- =============================================================================
|
||||
-- profiles, tenants, tenant_members, tenant_invites, tenant_features,
|
||||
-- tenant_feature_exceptions_log, saas_admins, owner_users, user_settings,
|
||||
-- company_profiles, dev_user_credentials
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE public.profiles (
|
||||
id uuid NOT NULL,
|
||||
role text DEFAULT 'tenant_member'::text NOT NULL,
|
||||
full_name text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
avatar_url text,
|
||||
phone text,
|
||||
bio text,
|
||||
language text DEFAULT 'pt-BR'::text NOT NULL,
|
||||
timezone text DEFAULT 'America/Sao_Paulo'::text NOT NULL,
|
||||
notify_system_email boolean DEFAULT true NOT NULL,
|
||||
notify_reminders boolean DEFAULT true NOT NULL,
|
||||
notify_news boolean DEFAULT false NOT NULL,
|
||||
account_type text DEFAULT 'free'::text NOT NULL,
|
||||
platform_roles text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
nickname text,
|
||||
work_description text,
|
||||
work_description_other text,
|
||||
site_url text,
|
||||
social_instagram text,
|
||||
social_youtube text,
|
||||
social_facebook text,
|
||||
social_x text,
|
||||
social_custom jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
CONSTRAINT profiles_account_type_check CHECK ((account_type = ANY (ARRAY['free'::text, 'patient'::text, 'therapist'::text, 'clinic'::text]))),
|
||||
CONSTRAINT profiles_role_check CHECK ((role = ANY (ARRAY['saas_admin'::text, 'tenant_member'::text, 'portal_user'::text, 'patient'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.tenants (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
name text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
kind text DEFAULT 'saas'::text NOT NULL,
|
||||
CONSTRAINT tenants_kind_check CHECK ((kind = ANY (ARRAY['therapist'::text, 'clinic_coworking'::text, 'clinic_reception'::text, 'clinic_full'::text, 'clinic'::text, 'saas'::text, 'supervisor'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.tenant_members (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
role text NOT NULL,
|
||||
status text DEFAULT 'active'::text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.tenant_invites (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
email text NOT NULL,
|
||||
role text NOT NULL,
|
||||
token uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
invited_by uuid,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
expires_at timestamp with time zone DEFAULT (now() + '7 days'::interval) NOT NULL,
|
||||
accepted_at timestamp with time zone,
|
||||
accepted_by uuid,
|
||||
revoked_at timestamp with time zone,
|
||||
revoked_by uuid,
|
||||
CONSTRAINT tenant_invites_role_check CHECK ((role = ANY (ARRAY['therapist'::text, 'secretary'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.tenant_features (
|
||||
tenant_id uuid NOT NULL,
|
||||
feature_key text NOT NULL,
|
||||
enabled boolean DEFAULT false NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.tenant_feature_exceptions_log (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
feature_key text NOT NULL,
|
||||
enabled boolean NOT NULL,
|
||||
reason text,
|
||||
created_by uuid,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.saas_admins (
|
||||
user_id uuid NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.owner_users (
|
||||
owner_id uuid NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
role text DEFAULT 'admin'::text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.user_settings (
|
||||
user_id uuid NOT NULL,
|
||||
theme_mode text DEFAULT 'dark'::text NOT NULL,
|
||||
preset text DEFAULT 'Aura'::text NOT NULL,
|
||||
primary_color text DEFAULT 'noir'::text NOT NULL,
|
||||
surface_color text DEFAULT 'slate'::text NOT NULL,
|
||||
menu_mode text DEFAULT 'static'::text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
layout_variant text DEFAULT 'classic'::text NOT NULL,
|
||||
CONSTRAINT user_settings_layout_variant_check CHECK ((layout_variant = ANY (ARRAY['classic'::text, 'rail'::text]))),
|
||||
CONSTRAINT user_settings_menu_mode_check CHECK ((menu_mode = ANY (ARRAY['static'::text, 'overlay'::text]))),
|
||||
CONSTRAINT user_settings_theme_mode_check CHECK ((theme_mode = ANY (ARRAY['light'::text, 'dark'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.company_profiles (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
nome_fantasia text,
|
||||
razao_social text,
|
||||
tipo_empresa text,
|
||||
cnpj text,
|
||||
ie text,
|
||||
im text,
|
||||
cep text,
|
||||
logradouro text,
|
||||
numero text,
|
||||
complemento text,
|
||||
bairro text,
|
||||
cidade text,
|
||||
estado text,
|
||||
email text,
|
||||
telefone text,
|
||||
site text,
|
||||
logo_url text,
|
||||
redes_sociais jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.dev_user_credentials (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
user_id uuid,
|
||||
email text NOT NULL,
|
||||
password_dev text NOT NULL,
|
||||
kind text DEFAULT 'custom'::text NOT NULL,
|
||||
note text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
199
database-novo/schema/04_tables/financial.sql
Normal file
199
database-novo/schema/04_tables/financial.sql
Normal file
@@ -0,0 +1,199 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Tables — Financeiro
|
||||
-- =============================================================================
|
||||
-- financial_records, financial_categories, financial_exceptions,
|
||||
-- payment_settings, professional_pricing, therapist_payouts,
|
||||
-- therapist_payout_records, services, insurance_plans, insurance_plan_services
|
||||
-- =============================================================================
|
||||
|
||||
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])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.financial_categories (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
name text NOT NULL,
|
||||
type public.financial_record_type DEFAULT 'receita'::public.financial_record_type NOT NULL,
|
||||
color text DEFAULT '#6366f1'::text,
|
||||
icon text DEFAULT 'pi pi-tag'::text,
|
||||
sort_order integer DEFAULT 0,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.financial_categories OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: financial_exceptions; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.financial_exceptions (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid,
|
||||
tenant_id uuid NOT NULL,
|
||||
exception_type text NOT NULL,
|
||||
charge_mode text NOT NULL,
|
||||
charge_value numeric(10,2),
|
||||
charge_pct numeric(5,2),
|
||||
min_hours_notice integer,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
updated_at timestamp with time zone DEFAULT now(),
|
||||
CONSTRAINT financial_exceptions_charge_chk CHECK ((charge_mode = ANY (ARRAY['none'::text, 'full'::text, 'fixed_fee'::text, 'percentage'::text]))),
|
||||
CONSTRAINT financial_exceptions_type_chk CHECK ((exception_type = ANY (ARRAY['patient_no_show'::text, 'patient_cancellation'::text, 'professional_cancellation'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.payment_settings (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
tenant_id uuid,
|
||||
pix_ativo boolean DEFAULT false NOT NULL,
|
||||
pix_tipo text DEFAULT 'cpf'::text NOT NULL,
|
||||
pix_chave text DEFAULT ''::text NOT NULL,
|
||||
pix_nome_titular text DEFAULT ''::text NOT NULL,
|
||||
deposito_ativo boolean DEFAULT false NOT NULL,
|
||||
deposito_banco text DEFAULT ''::text NOT NULL,
|
||||
deposito_agencia text DEFAULT ''::text NOT NULL,
|
||||
deposito_conta text DEFAULT ''::text NOT NULL,
|
||||
deposito_tipo_conta text DEFAULT 'corrente'::text NOT NULL,
|
||||
deposito_titular text DEFAULT ''::text NOT NULL,
|
||||
deposito_cpf_cnpj text DEFAULT ''::text NOT NULL,
|
||||
dinheiro_ativo boolean DEFAULT false NOT NULL,
|
||||
cartao_ativo boolean DEFAULT false NOT NULL,
|
||||
cartao_instrucao text DEFAULT ''::text NOT NULL,
|
||||
convenio_ativo boolean DEFAULT false NOT NULL,
|
||||
convenio_lista text DEFAULT ''::text NOT NULL,
|
||||
observacoes_pagamento text DEFAULT ''::text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
updated_at timestamp with time zone DEFAULT now()
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.professional_pricing (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
determined_commitment_id uuid,
|
||||
price numeric(10,2) NOT NULL,
|
||||
notes text,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
updated_at timestamp with time zone DEFAULT now()
|
||||
);
|
||||
|
||||
|
||||
|
||||
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])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.therapist_payout_records (
|
||||
payout_id uuid NOT NULL,
|
||||
financial_record_id uuid NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.services (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
name text NOT NULL,
|
||||
description text,
|
||||
price numeric(10,2) NOT NULL,
|
||||
duration_min integer,
|
||||
active boolean DEFAULT true NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
updated_at timestamp with time zone DEFAULT now()
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.insurance_plan_services (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
insurance_plan_id uuid NOT NULL,
|
||||
name text NOT NULL,
|
||||
value numeric(10,2) NOT NULL,
|
||||
active boolean DEFAULT true NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
updated_at timestamp with time zone DEFAULT now()
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.insurance_plan_services OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: insurance_plans; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.insurance_plans (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
name text NOT NULL,
|
||||
notes text,
|
||||
default_value numeric(10,2),
|
||||
active boolean DEFAULT true NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
updated_at timestamp with time zone DEFAULT now()
|
||||
);
|
||||
|
||||
|
||||
500
database-novo/schema/04_tables/infra.sql
Normal file
500
database-novo/schema/04_tables/infra.sql
Normal file
@@ -0,0 +1,500 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Tables — Infraestrutura (realtime, storage, supabase_functions)
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE _realtime.extensions (
|
||||
id uuid NOT NULL,
|
||||
type text,
|
||||
settings jsonb,
|
||||
tenant_external_id text,
|
||||
inserted_at timestamp(0) without time zone NOT NULL,
|
||||
updated_at timestamp(0) without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE _realtime.extensions OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: schema_migrations; Type: TABLE; Schema: _realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE _realtime.schema_migrations (
|
||||
version bigint NOT NULL,
|
||||
inserted_at timestamp(0) without time zone
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE _realtime.schema_migrations OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: tenants; Type: TABLE; Schema: _realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE _realtime.tenants (
|
||||
id uuid NOT NULL,
|
||||
name text,
|
||||
external_id text,
|
||||
jwt_secret text,
|
||||
max_concurrent_users integer DEFAULT 200 NOT NULL,
|
||||
inserted_at timestamp(0) without time zone NOT NULL,
|
||||
updated_at timestamp(0) without time zone NOT NULL,
|
||||
max_events_per_second integer DEFAULT 100 NOT NULL,
|
||||
postgres_cdc_default text DEFAULT 'postgres_cdc_rls'::text,
|
||||
max_bytes_per_second integer DEFAULT 100000 NOT NULL,
|
||||
max_channels_per_client integer DEFAULT 100 NOT NULL,
|
||||
max_joins_per_second integer DEFAULT 500 NOT NULL,
|
||||
suspend boolean DEFAULT false,
|
||||
jwt_jwks jsonb,
|
||||
notify_private_alpha boolean DEFAULT false,
|
||||
private_only boolean DEFAULT false NOT NULL,
|
||||
migrations_ran integer DEFAULT 0,
|
||||
broadcast_adapter character varying(255) DEFAULT 'gen_rpc'::character varying,
|
||||
max_presence_events_per_second integer DEFAULT 1000,
|
||||
max_payload_size_in_kb integer DEFAULT 3000,
|
||||
CONSTRAINT jwt_secret_or_jwt_jwks_required CHECK (((jwt_secret IS NOT NULL) OR (jwt_jwks IS NOT NULL)))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE _realtime.tenants OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: audit_log_entries; Type: TABLE; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
|
||||
CREATE TABLE realtime.messages (
|
||||
topic text NOT NULL,
|
||||
extension text NOT NULL,
|
||||
payload jsonb,
|
||||
event text,
|
||||
private boolean DEFAULT false,
|
||||
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||
)
|
||||
PARTITION BY RANGE (inserted_at);
|
||||
|
||||
|
||||
ALTER TABLE realtime.messages OWNER TO supabase_realtime_admin;
|
||||
|
||||
--
|
||||
-- Name: messages_2026_03_20; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE realtime.messages_2026_03_20 (
|
||||
topic text NOT NULL,
|
||||
extension text NOT NULL,
|
||||
payload jsonb,
|
||||
event text,
|
||||
private boolean DEFAULT false,
|
||||
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE realtime.messages_2026_03_20 OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: messages_2026_03_21; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE realtime.messages_2026_03_21 (
|
||||
topic text NOT NULL,
|
||||
extension text NOT NULL,
|
||||
payload jsonb,
|
||||
event text,
|
||||
private boolean DEFAULT false,
|
||||
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE realtime.messages_2026_03_21 OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: messages_2026_03_22; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE realtime.messages_2026_03_22 (
|
||||
topic text NOT NULL,
|
||||
extension text NOT NULL,
|
||||
payload jsonb,
|
||||
event text,
|
||||
private boolean DEFAULT false,
|
||||
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE realtime.messages_2026_03_22 OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: messages_2026_03_23; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE realtime.messages_2026_03_23 (
|
||||
topic text NOT NULL,
|
||||
extension text NOT NULL,
|
||||
payload jsonb,
|
||||
event text,
|
||||
private boolean DEFAULT false,
|
||||
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE realtime.messages_2026_03_23 OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: messages_2026_03_24; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE realtime.messages_2026_03_24 (
|
||||
topic text NOT NULL,
|
||||
extension text NOT NULL,
|
||||
payload jsonb,
|
||||
event text,
|
||||
private boolean DEFAULT false,
|
||||
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE realtime.messages_2026_03_24 OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: messages_2026_03_25; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE realtime.messages_2026_03_25 (
|
||||
topic text NOT NULL,
|
||||
extension text NOT NULL,
|
||||
payload jsonb,
|
||||
event text,
|
||||
private boolean DEFAULT false,
|
||||
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE realtime.messages_2026_03_25 OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: messages_2026_03_26; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE realtime.messages_2026_03_26 (
|
||||
topic text NOT NULL,
|
||||
extension text NOT NULL,
|
||||
payload jsonb,
|
||||
event text,
|
||||
private boolean DEFAULT false,
|
||||
updated_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
inserted_at timestamp without time zone DEFAULT now() NOT NULL,
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE realtime.messages_2026_03_26 OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: schema_migrations; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE realtime.schema_migrations (
|
||||
version bigint NOT NULL,
|
||||
inserted_at timestamp(0) without time zone
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE realtime.schema_migrations OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: subscription; Type: TABLE; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE realtime.subscription (
|
||||
id bigint NOT NULL,
|
||||
subscription_id uuid NOT NULL,
|
||||
entity regclass NOT NULL,
|
||||
filters realtime.user_defined_filter[] DEFAULT '{}'::realtime.user_defined_filter[] NOT NULL,
|
||||
claims jsonb NOT NULL,
|
||||
claims_role regrole GENERATED ALWAYS AS (realtime.to_regrole((claims ->> 'role'::text))) STORED NOT NULL,
|
||||
created_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE realtime.subscription OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: subscription_id_seq; Type: SEQUENCE; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
ALTER TABLE realtime.subscription ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
|
||||
SEQUENCE NAME realtime.subscription_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: buckets; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
|
||||
CREATE TABLE storage.buckets (
|
||||
id text NOT NULL,
|
||||
name text NOT NULL,
|
||||
owner uuid,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
updated_at timestamp with time zone DEFAULT now(),
|
||||
public boolean DEFAULT false,
|
||||
avif_autodetection boolean DEFAULT false,
|
||||
file_size_limit bigint,
|
||||
allowed_mime_types text[],
|
||||
owner_id text,
|
||||
type storage.buckettype DEFAULT 'STANDARD'::storage.buckettype NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE storage.buckets OWNER TO supabase_storage_admin;
|
||||
|
||||
--
|
||||
-- Name: COLUMN buckets.owner; Type: COMMENT; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN storage.buckets.owner IS 'Field is deprecated, use owner_id instead';
|
||||
|
||||
|
||||
--
|
||||
-- Name: buckets_analytics; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
CREATE TABLE storage.buckets_analytics (
|
||||
name text NOT NULL,
|
||||
type storage.buckettype DEFAULT 'ANALYTICS'::storage.buckettype NOT NULL,
|
||||
format text DEFAULT 'ICEBERG'::text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
deleted_at timestamp with time zone
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE storage.buckets_analytics OWNER TO supabase_storage_admin;
|
||||
|
||||
--
|
||||
-- Name: buckets_vectors; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
CREATE TABLE storage.buckets_vectors (
|
||||
id text NOT NULL,
|
||||
type storage.buckettype DEFAULT 'VECTOR'::storage.buckettype NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE storage.buckets_vectors OWNER TO supabase_storage_admin;
|
||||
|
||||
--
|
||||
-- Name: iceberg_namespaces; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
CREATE TABLE storage.iceberg_namespaces (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
bucket_name text NOT NULL,
|
||||
name text NOT NULL COLLATE pg_catalog."C",
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
catalog_id uuid NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE storage.iceberg_namespaces OWNER TO supabase_storage_admin;
|
||||
|
||||
--
|
||||
-- Name: iceberg_tables; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
CREATE TABLE storage.iceberg_tables (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
namespace_id uuid NOT NULL,
|
||||
bucket_name text NOT NULL,
|
||||
name text NOT NULL COLLATE pg_catalog."C",
|
||||
location text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
remote_table_id text,
|
||||
shard_key text,
|
||||
shard_id text,
|
||||
catalog_id uuid NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE storage.iceberg_tables OWNER TO supabase_storage_admin;
|
||||
|
||||
--
|
||||
-- Name: migrations; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
CREATE TABLE storage.migrations (
|
||||
id integer NOT NULL,
|
||||
name character varying(100) NOT NULL,
|
||||
hash character varying(40) NOT NULL,
|
||||
executed_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE storage.migrations OWNER TO supabase_storage_admin;
|
||||
|
||||
--
|
||||
-- Name: objects; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
CREATE TABLE storage.objects (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
bucket_id text,
|
||||
name text,
|
||||
owner uuid,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
updated_at timestamp with time zone DEFAULT now(),
|
||||
last_accessed_at timestamp with time zone DEFAULT now(),
|
||||
metadata jsonb,
|
||||
path_tokens text[] GENERATED ALWAYS AS (string_to_array(name, '/'::text)) STORED,
|
||||
version text,
|
||||
owner_id text,
|
||||
user_metadata jsonb
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE storage.objects OWNER TO supabase_storage_admin;
|
||||
|
||||
--
|
||||
-- Name: COLUMN objects.owner; Type: COMMENT; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN storage.objects.owner IS 'Field is deprecated, use owner_id instead';
|
||||
|
||||
|
||||
--
|
||||
-- Name: s3_multipart_uploads; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
CREATE TABLE storage.s3_multipart_uploads (
|
||||
id text NOT NULL,
|
||||
in_progress_size bigint DEFAULT 0 NOT NULL,
|
||||
upload_signature text NOT NULL,
|
||||
bucket_id text NOT NULL,
|
||||
key text NOT NULL COLLATE pg_catalog."C",
|
||||
version text NOT NULL,
|
||||
owner_id text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
user_metadata jsonb
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE storage.s3_multipart_uploads OWNER TO supabase_storage_admin;
|
||||
|
||||
--
|
||||
-- Name: s3_multipart_uploads_parts; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
CREATE TABLE storage.s3_multipart_uploads_parts (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
upload_id text NOT NULL,
|
||||
size bigint DEFAULT 0 NOT NULL,
|
||||
part_number integer NOT NULL,
|
||||
bucket_id text NOT NULL,
|
||||
key text NOT NULL COLLATE pg_catalog."C",
|
||||
etag text NOT NULL,
|
||||
owner_id text,
|
||||
version text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE storage.s3_multipart_uploads_parts OWNER TO supabase_storage_admin;
|
||||
|
||||
--
|
||||
-- Name: vector_indexes; Type: TABLE; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
CREATE TABLE storage.vector_indexes (
|
||||
id text DEFAULT gen_random_uuid() NOT NULL,
|
||||
name text NOT NULL COLLATE pg_catalog."C",
|
||||
bucket_id text NOT NULL,
|
||||
data_type text NOT NULL,
|
||||
dimension integer NOT NULL,
|
||||
distance_metric text NOT NULL,
|
||||
metadata_configuration jsonb,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE supabase_functions.hooks (
|
||||
id bigint NOT NULL,
|
||||
hook_table_id integer NOT NULL,
|
||||
hook_name text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
request_id bigint
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE supabase_functions.hooks OWNER TO supabase_functions_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE hooks; Type: COMMENT; Schema: supabase_functions; Owner: supabase_functions_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: hooks_id_seq; Type: SEQUENCE; Schema: supabase_functions; Owner: supabase_functions_admin
|
||||
--
|
||||
|
||||
CREATE SEQUENCE supabase_functions.hooks_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE supabase_functions.hooks_id_seq OWNER TO supabase_functions_admin;
|
||||
|
||||
--
|
||||
-- Name: hooks_id_seq; Type: SEQUENCE OWNED BY; Schema: supabase_functions; Owner: supabase_functions_admin
|
||||
--
|
||||
|
||||
ALTER SEQUENCE supabase_functions.hooks_id_seq OWNED BY supabase_functions.hooks.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: migrations; Type: TABLE; Schema: supabase_functions; Owner: supabase_functions_admin
|
||||
--
|
||||
|
||||
CREATE TABLE supabase_functions.migrations (
|
||||
version text NOT NULL,
|
||||
inserted_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE supabase_functions.migrations OWNER TO supabase_functions_admin;
|
||||
|
||||
--
|
||||
-- Name: messages_2026_03_20; Type: TABLE ATTACH; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
267
database-novo/schema/04_tables/notifications.sql
Normal file
267
database-novo/schema/04_tables/notifications.sql
Normal file
@@ -0,0 +1,267 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Tables — Notificações + Email Templates
|
||||
-- =============================================================================
|
||||
-- notification_channels, notification_logs, notification_preferences,
|
||||
-- notification_queue, notification_schedules, notification_templates,
|
||||
-- notifications, email_templates_global, email_templates_tenant,
|
||||
-- email_layout_config
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE public.notification_channels (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
channel text NOT NULL,
|
||||
provider text NOT NULL,
|
||||
is_active boolean DEFAULT false NOT NULL,
|
||||
display_name text,
|
||||
sender_address text,
|
||||
credentials jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
connection_status text DEFAULT 'disconnected'::text,
|
||||
last_health_check timestamp with time zone,
|
||||
metadata jsonb DEFAULT '{}'::jsonb,
|
||||
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,
|
||||
CONSTRAINT notification_channels_channel_check CHECK ((channel = ANY (ARRAY['whatsapp'::text, 'email'::text, 'sms'::text]))),
|
||||
CONSTRAINT notification_channels_connection_status_check CHECK ((connection_status = ANY (ARRAY['connected'::text, 'disconnected'::text, 'connecting'::text, 'qr_pending'::text, 'error'::text]))),
|
||||
CONSTRAINT notification_channels_provider_check CHECK ((provider = ANY (ARRAY['evolution_api'::text, 'meta_official'::text, 'twilio'::text, 'zenvia'::text, 'sendgrid'::text, 'resend'::text, 'smtp'::text, 'zapi'::text])))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.notification_channels OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: notification_logs; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.notification_logs (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
queue_id uuid,
|
||||
agenda_evento_id uuid,
|
||||
patient_id uuid NOT NULL,
|
||||
channel text NOT NULL,
|
||||
template_key text NOT NULL,
|
||||
schedule_key text,
|
||||
recipient_address text NOT NULL,
|
||||
resolved_message text,
|
||||
resolved_vars jsonb,
|
||||
status text NOT NULL,
|
||||
provider text,
|
||||
provider_message_id text,
|
||||
provider_status text,
|
||||
provider_response jsonb,
|
||||
sent_at timestamp with time zone,
|
||||
delivered_at timestamp with time zone,
|
||||
read_at timestamp with time zone,
|
||||
failed_at timestamp with time zone,
|
||||
failure_reason text,
|
||||
estimated_cost_brl numeric(8,4) DEFAULT 0,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT notification_logs_status_check CHECK ((status = ANY (ARRAY['sent'::text, 'delivered'::text, 'read'::text, 'failed'::text, 'bounced'::text, 'opted_out'::text])))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.notification_logs OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: notification_preferences; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.notification_preferences (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
patient_id uuid NOT NULL,
|
||||
whatsapp_opt_in boolean DEFAULT true NOT NULL,
|
||||
email_opt_in boolean DEFAULT true NOT NULL,
|
||||
sms_opt_in boolean DEFAULT false NOT NULL,
|
||||
preferred_time_start time without time zone DEFAULT '08:00:00'::time without time zone,
|
||||
preferred_time_end time without time zone DEFAULT '20:00:00'::time without time zone,
|
||||
lgpd_consent_given boolean DEFAULT false NOT NULL,
|
||||
lgpd_consent_date timestamp with time zone,
|
||||
lgpd_consent_version text,
|
||||
lgpd_consent_ip inet,
|
||||
lgpd_opt_out_date timestamp with time zone,
|
||||
lgpd_opt_out_reason 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
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.notification_preferences OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: notification_queue; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.notification_queue (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
agenda_evento_id uuid,
|
||||
patient_id uuid NOT NULL,
|
||||
channel text NOT NULL,
|
||||
template_key text NOT NULL,
|
||||
schedule_key text NOT NULL,
|
||||
resolved_vars jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
recipient_address text NOT NULL,
|
||||
status text DEFAULT 'pendente'::text NOT NULL,
|
||||
scheduled_at timestamp with time zone NOT NULL,
|
||||
sent_at timestamp with time zone,
|
||||
next_retry_at timestamp with time zone,
|
||||
attempts integer DEFAULT 0 NOT NULL,
|
||||
max_attempts integer DEFAULT 5 NOT NULL,
|
||||
last_error text,
|
||||
idempotency_key text NOT NULL,
|
||||
provider_message_id text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT notification_queue_channel_check CHECK ((channel = ANY (ARRAY['whatsapp'::text, 'email'::text, 'sms'::text]))),
|
||||
CONSTRAINT notification_queue_status_check CHECK ((status = ANY (ARRAY['pendente'::text, 'processando'::text, 'enviado'::text, 'falhou'::text, 'cancelado'::text, 'ignorado'::text])))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.notification_queue OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: notification_schedules; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.notification_schedules (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
schedule_key text NOT NULL,
|
||||
event_type text NOT NULL,
|
||||
trigger_type text NOT NULL,
|
||||
offset_minutes integer DEFAULT 0,
|
||||
whatsapp_enabled boolean DEFAULT true NOT NULL,
|
||||
email_enabled boolean DEFAULT true NOT NULL,
|
||||
sms_enabled boolean DEFAULT false NOT NULL,
|
||||
allowed_time_start time without time zone DEFAULT '08:00:00'::time without time zone,
|
||||
allowed_time_end time without time zone DEFAULT '20:00:00'::time without time zone,
|
||||
skip_weekends boolean DEFAULT false,
|
||||
skip_holidays boolean DEFAULT false,
|
||||
is_active boolean DEFAULT true NOT NULL,
|
||||
sort_order integer DEFAULT 0,
|
||||
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,
|
||||
CONSTRAINT notification_schedules_event_type_check CHECK ((event_type = ANY (ARRAY['lembrete_sessao'::text, 'confirmacao_sessao'::text, 'cancelamento_sessao'::text, 'reagendamento'::text, 'cobranca_pendente'::text, 'boas_vindas_paciente'::text]))),
|
||||
CONSTRAINT notification_schedules_trigger_type_check CHECK ((trigger_type = ANY (ARRAY['before_event'::text, 'after_event'::text, 'immediate'::text])))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.notification_schedules OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: notification_templates; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.notification_templates (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid,
|
||||
owner_id uuid,
|
||||
key text NOT NULL,
|
||||
domain text NOT NULL,
|
||||
channel text NOT NULL,
|
||||
event_type text NOT NULL,
|
||||
body_text text NOT NULL,
|
||||
meta_template_name text,
|
||||
meta_template_namespace text,
|
||||
meta_components jsonb,
|
||||
meta_status text DEFAULT 'draft'::text,
|
||||
variables jsonb DEFAULT '[]'::jsonb,
|
||||
version integer DEFAULT 1 NOT NULL,
|
||||
is_active boolean DEFAULT true NOT NULL,
|
||||
is_default boolean DEFAULT false NOT NULL,
|
||||
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,
|
||||
CONSTRAINT notification_templates_channel_check CHECK ((channel = ANY (ARRAY['whatsapp'::text, 'sms'::text]))),
|
||||
CONSTRAINT notification_templates_domain_check CHECK ((domain = ANY (ARRAY['session'::text, 'intake'::text, 'billing'::text, 'system'::text]))),
|
||||
CONSTRAINT notification_templates_event_type_check CHECK ((event_type = ANY (ARRAY['lembrete_sessao'::text, 'confirmacao_sessao'::text, 'cancelamento_sessao'::text, 'reagendamento'::text, 'cobranca_pendente'::text, 'boas_vindas_paciente'::text, 'intake_recebido'::text, 'intake_aprovado'::text, 'intake_rejeitado'::text]))),
|
||||
CONSTRAINT notification_templates_meta_status_check CHECK ((meta_status = ANY (ARRAY['draft'::text, 'pending_approval'::text, 'approved'::text, 'rejected'::text])))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.notification_templates OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: notifications; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.notifications (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
tenant_id uuid,
|
||||
type text NOT NULL,
|
||||
ref_id uuid,
|
||||
ref_table text,
|
||||
payload jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
read_at timestamp with time zone,
|
||||
archived boolean DEFAULT false NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT notifications_type_check CHECK ((type = ANY (ARRAY['new_scheduling'::text, 'new_patient'::text, 'recurrence_alert'::text, 'session_status'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.email_layout_config (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
header_config jsonb DEFAULT '{"layout": null, "content": "", "enabled": false}'::jsonb NOT NULL,
|
||||
footer_config jsonb DEFAULT '{"layout": null, "content": "", "enabled": false}'::jsonb NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.email_layout_config OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: email_templates_global; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.email_templates_global (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
key text NOT NULL,
|
||||
domain text NOT NULL,
|
||||
channel text DEFAULT 'email'::text NOT NULL,
|
||||
subject text NOT NULL,
|
||||
body_html text NOT NULL,
|
||||
body_text text,
|
||||
version integer DEFAULT 1 NOT NULL,
|
||||
is_active boolean DEFAULT true NOT NULL,
|
||||
variables jsonb,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.email_templates_global OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: email_templates_tenant; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.email_templates_tenant (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
owner_id uuid,
|
||||
template_key text NOT NULL,
|
||||
subject text,
|
||||
body_html text,
|
||||
body_text text,
|
||||
enabled boolean DEFAULT true NOT NULL,
|
||||
synced_version integer,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
185
database-novo/schema/04_tables/patients.sql
Normal file
185
database-novo/schema/04_tables/patients.sql
Normal file
@@ -0,0 +1,185 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Tables — Pacientes
|
||||
-- =============================================================================
|
||||
-- patients, patient_groups, patient_group_patient, patient_tags,
|
||||
-- patient_patient_tag, patient_intake_requests, patient_invites,
|
||||
-- patient_discounts
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE public.patients (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
nome_completo text NOT NULL,
|
||||
email_principal text,
|
||||
telefone text,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
owner_id uuid,
|
||||
avatar_url text,
|
||||
status text DEFAULT 'Ativo'::text,
|
||||
last_attended_at timestamp with time zone,
|
||||
is_native boolean DEFAULT false,
|
||||
naturalidade text,
|
||||
data_nascimento date,
|
||||
rg text,
|
||||
cpf text,
|
||||
identification_color text,
|
||||
genero text,
|
||||
estado_civil text,
|
||||
email_alternativo text,
|
||||
pais text DEFAULT 'Brasil'::text,
|
||||
cep text,
|
||||
cidade text,
|
||||
estado text,
|
||||
endereco text,
|
||||
numero text,
|
||||
bairro text,
|
||||
complemento text,
|
||||
escolaridade text,
|
||||
profissao text,
|
||||
nome_parente text,
|
||||
grau_parentesco text,
|
||||
telefone_alternativo text,
|
||||
onde_nos_conheceu text,
|
||||
encaminhado_por text,
|
||||
nome_responsavel text,
|
||||
telefone_responsavel text,
|
||||
cpf_responsavel text,
|
||||
observacao_responsavel text,
|
||||
cobranca_no_responsavel boolean DEFAULT false,
|
||||
observacoes text,
|
||||
notas_internas text,
|
||||
updated_at timestamp with time zone DEFAULT now(),
|
||||
telefone_parente text,
|
||||
tenant_id uuid NOT NULL,
|
||||
responsible_member_id uuid NOT NULL,
|
||||
user_id uuid,
|
||||
patient_scope text DEFAULT 'clinic'::text NOT NULL,
|
||||
therapist_member_id uuid,
|
||||
CONSTRAINT cpf_responsavel_format_check CHECK (((cpf_responsavel IS NULL) OR (cpf_responsavel ~ '^\d{11}$'::text))),
|
||||
CONSTRAINT patients_cpf_format_check CHECK (((cpf IS NULL) OR (cpf ~ '^\d{11}$'::text))),
|
||||
CONSTRAINT patients_patient_scope_check CHECK ((patient_scope = ANY (ARRAY['clinic'::text, 'therapist'::text]))),
|
||||
CONSTRAINT patients_status_check CHECK ((status = ANY (ARRAY['Ativo'::text, 'Inativo'::text, 'Alta'::text, 'Encaminhado'::text, 'Arquivado'::text]))),
|
||||
CONSTRAINT patients_therapist_scope_consistency CHECK ((((patient_scope = 'clinic'::text) AND (therapist_member_id IS NULL)) OR ((patient_scope = 'therapist'::text) AND (therapist_member_id IS NOT NULL))))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.patient_groups (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
nome text NOT NULL,
|
||||
descricao text,
|
||||
cor text,
|
||||
is_active boolean DEFAULT true NOT NULL,
|
||||
is_system boolean DEFAULT false NOT NULL,
|
||||
owner_id uuid DEFAULT auth.uid() NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
therapist_id uuid,
|
||||
tenant_id uuid NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.patient_group_patient (
|
||||
patient_group_id uuid NOT NULL,
|
||||
patient_id uuid NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
tenant_id uuid NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.patient_tags (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
nome text NOT NULL,
|
||||
cor text,
|
||||
is_padrao boolean DEFAULT false NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone,
|
||||
tenant_id uuid NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.patient_patient_tag (
|
||||
owner_id uuid NOT NULL,
|
||||
patient_id uuid NOT NULL,
|
||||
tag_id uuid NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
tenant_id uuid NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.patient_intake_requests (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
token text NOT NULL,
|
||||
consent boolean DEFAULT false NOT NULL,
|
||||
status text DEFAULT 'new'::text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
converted_patient_id uuid,
|
||||
rejected_reason text,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
cpf text,
|
||||
rg text,
|
||||
cep text,
|
||||
nome_completo text,
|
||||
email_principal text,
|
||||
telefone text,
|
||||
pais text,
|
||||
cidade text,
|
||||
estado text,
|
||||
endereco text,
|
||||
numero text,
|
||||
bairro text,
|
||||
complemento text,
|
||||
data_nascimento date,
|
||||
naturalidade text,
|
||||
genero text,
|
||||
estado_civil text,
|
||||
onde_nos_conheceu text,
|
||||
encaminhado_por text,
|
||||
observacoes text,
|
||||
notas_internas text,
|
||||
email_alternativo text,
|
||||
telefone_alternativo text,
|
||||
profissao text,
|
||||
escolaridade text,
|
||||
nacionalidade text,
|
||||
avatar_url text,
|
||||
tenant_id uuid,
|
||||
CONSTRAINT chk_intakes_status CHECK ((status = ANY (ARRAY['new'::text, 'converted'::text, 'rejected'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.patient_invites (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
token text NOT NULL,
|
||||
active boolean DEFAULT true NOT NULL,
|
||||
expires_at timestamp with time zone,
|
||||
max_uses integer,
|
||||
uses integer DEFAULT 0 NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
tenant_id uuid
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.patient_discounts (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
patient_id uuid NOT NULL,
|
||||
discount_pct numeric(5,2) DEFAULT 0,
|
||||
discount_flat numeric(10,2) DEFAULT 0,
|
||||
reason text,
|
||||
active boolean DEFAULT true NOT NULL,
|
||||
active_from timestamp with time zone DEFAULT now(),
|
||||
active_to timestamp with time zone,
|
||||
created_at timestamp with time zone DEFAULT now()
|
||||
);
|
||||
|
||||
|
||||
371
database-novo/schema/04_tables/plans_billing.sql
Normal file
371
database-novo/schema/04_tables/plans_billing.sql
Normal file
@@ -0,0 +1,371 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Tables — Plans, Billing, Subscriptions
|
||||
-- =============================================================================
|
||||
-- plans, plan_prices, plan_features, plan_public, plan_public_bullets,
|
||||
-- features, entitlements_invalidation, subscriptions, subscription_events,
|
||||
-- subscription_intents_personal, subscription_intents_tenant,
|
||||
-- subscription_intents_legacy, billing_contracts,
|
||||
-- addon_credits, addon_products, addon_transactions,
|
||||
-- modules, module_features, tenant_modules
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE public.plans (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
key text NOT NULL,
|
||||
name text NOT NULL,
|
||||
description text,
|
||||
is_active boolean DEFAULT true NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
price_cents integer DEFAULT 0 NOT NULL,
|
||||
currency text DEFAULT 'BRL'::text NOT NULL,
|
||||
billing_interval text DEFAULT 'month'::text NOT NULL,
|
||||
target text,
|
||||
max_supervisees integer,
|
||||
CONSTRAINT plans_target_check CHECK ((target = ANY (ARRAY['patient'::text, 'therapist'::text, 'clinic'::text, 'supervisor'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.plan_prices (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
plan_id uuid NOT NULL,
|
||||
currency text DEFAULT 'BRL'::text NOT NULL,
|
||||
"interval" text NOT NULL,
|
||||
amount_cents integer NOT NULL,
|
||||
is_active boolean DEFAULT true NOT NULL,
|
||||
active_from timestamp with time zone DEFAULT now() NOT NULL,
|
||||
active_to timestamp with time zone,
|
||||
source text DEFAULT 'manual'::text NOT NULL,
|
||||
provider text,
|
||||
provider_price_id text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT plan_prices_amount_cents_check CHECK ((amount_cents >= 0)),
|
||||
CONSTRAINT plan_prices_interval_check CHECK (("interval" = ANY (ARRAY['month'::text, 'year'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.plan_features (
|
||||
plan_id uuid NOT NULL,
|
||||
feature_id uuid NOT NULL,
|
||||
enabled boolean DEFAULT true NOT NULL,
|
||||
limits jsonb,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.plan_public (
|
||||
plan_id uuid NOT NULL,
|
||||
public_name text DEFAULT ''::text NOT NULL,
|
||||
public_description text DEFAULT ''::text NOT NULL,
|
||||
badge text,
|
||||
is_featured boolean DEFAULT false NOT NULL,
|
||||
is_visible boolean DEFAULT true NOT NULL,
|
||||
sort_order integer DEFAULT 0 NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.plan_public_bullets (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
plan_id uuid NOT NULL,
|
||||
text text NOT NULL,
|
||||
sort_order integer DEFAULT 0 NOT NULL,
|
||||
highlight boolean DEFAULT false NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.features (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
key text NOT NULL,
|
||||
description text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
descricao text DEFAULT ''::text NOT NULL,
|
||||
name text DEFAULT ''::text NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.features OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: COLUMN features.descricao; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN public.features.descricao IS 'Descrição humana da feature (exibição no admin e documentação).';
|
||||
|
||||
|
||||
--
|
||||
-- Name: feriados; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
|
||||
CREATE TABLE public.entitlements_invalidation (
|
||||
owner_id uuid NOT NULL,
|
||||
changed_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.subscriptions (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
user_id uuid,
|
||||
plan_id uuid NOT NULL,
|
||||
status text DEFAULT 'active'::text NOT NULL,
|
||||
current_period_start timestamp with time zone,
|
||||
current_period_end timestamp with time zone,
|
||||
cancel_at_period_end boolean DEFAULT false NOT NULL,
|
||||
provider text DEFAULT 'manual'::text NOT NULL,
|
||||
provider_customer_id text,
|
||||
provider_subscription_id text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
tenant_id uuid,
|
||||
plan_key text,
|
||||
"interval" text,
|
||||
source text DEFAULT 'manual'::text NOT NULL,
|
||||
started_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
canceled_at timestamp with time zone,
|
||||
activated_at timestamp with time zone,
|
||||
past_due_since timestamp with time zone,
|
||||
suspended_at timestamp with time zone,
|
||||
suspended_reason text,
|
||||
cancelled_at timestamp with time zone,
|
||||
cancel_reason text,
|
||||
expired_at timestamp with time zone,
|
||||
CONSTRAINT subscriptions_interval_check CHECK (("interval" = ANY (ARRAY['month'::text, 'year'::text]))),
|
||||
CONSTRAINT subscriptions_owner_xor CHECK ((((tenant_id IS NOT NULL) AND (user_id IS NULL)) OR ((tenant_id IS NULL) AND (user_id IS NOT NULL)))),
|
||||
CONSTRAINT subscriptions_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'active'::text, 'past_due'::text, 'suspended'::text, 'cancelled'::text, 'expired'::text])))
|
||||
);
|
||||
|
||||
CREATE TABLE public.subscription_events (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
subscription_id uuid NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
event_type text NOT NULL,
|
||||
old_plan_id uuid,
|
||||
new_plan_id uuid,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
created_by uuid,
|
||||
source text DEFAULT 'admin_ui'::text,
|
||||
reason text,
|
||||
metadata jsonb,
|
||||
owner_type text NOT NULL,
|
||||
owner_ref uuid NOT NULL,
|
||||
CONSTRAINT subscription_events_owner_ref_consistency_chk CHECK ((owner_id = owner_ref)),
|
||||
CONSTRAINT subscription_events_owner_type_chk CHECK (((owner_type IS NULL) OR (owner_type = ANY (ARRAY['clinic'::text, 'therapist'::text]))))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.subscription_intents_personal (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
created_by_user_id uuid,
|
||||
email text NOT NULL,
|
||||
plan_id uuid NOT NULL,
|
||||
plan_key text,
|
||||
"interval" text,
|
||||
amount_cents integer,
|
||||
currency text,
|
||||
status text DEFAULT 'new'::text NOT NULL,
|
||||
source text DEFAULT 'manual'::text NOT NULL,
|
||||
notes text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
paid_at timestamp with time zone,
|
||||
subscription_id uuid,
|
||||
CONSTRAINT sint_personal_interval_check CHECK ((("interval" IS NULL) OR ("interval" = ANY (ARRAY['month'::text, 'year'::text])))),
|
||||
CONSTRAINT sint_personal_status_check CHECK ((status = ANY (ARRAY['new'::text, 'waiting_payment'::text, 'paid'::text, 'canceled'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.subscription_intents_tenant (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
created_by_user_id uuid,
|
||||
email text NOT NULL,
|
||||
plan_id uuid NOT NULL,
|
||||
plan_key text,
|
||||
"interval" text,
|
||||
amount_cents integer,
|
||||
currency text,
|
||||
status text DEFAULT 'new'::text NOT NULL,
|
||||
source text DEFAULT 'manual'::text NOT NULL,
|
||||
notes text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
paid_at timestamp with time zone,
|
||||
tenant_id uuid NOT NULL,
|
||||
subscription_id uuid,
|
||||
CONSTRAINT sint_tenant_interval_check CHECK ((("interval" IS NULL) OR ("interval" = ANY (ARRAY['month'::text, 'year'::text])))),
|
||||
CONSTRAINT sint_tenant_status_check CHECK ((status = ANY (ARRAY['new'::text, 'waiting_payment'::text, 'paid'::text, 'canceled'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.subscription_intents_legacy (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
user_id uuid,
|
||||
email text,
|
||||
plan_key text NOT NULL,
|
||||
"interval" text NOT NULL,
|
||||
amount_cents integer NOT NULL,
|
||||
currency text DEFAULT 'BRL'::text NOT NULL,
|
||||
status text DEFAULT 'new'::text NOT NULL,
|
||||
source text DEFAULT 'landing'::text NOT NULL,
|
||||
notes text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
paid_at timestamp with time zone,
|
||||
tenant_id uuid NOT NULL,
|
||||
created_by_user_id uuid,
|
||||
CONSTRAINT subscription_intents_interval_check CHECK (("interval" = ANY (ARRAY['month'::text, 'year'::text]))),
|
||||
CONSTRAINT subscription_intents_status_check CHECK ((status = ANY (ARRAY['new'::text, 'waiting_payment'::text, 'paid'::text, 'canceled'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.billing_contracts (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
patient_id uuid NOT NULL,
|
||||
type text NOT NULL,
|
||||
total_sessions integer,
|
||||
sessions_used integer DEFAULT 0,
|
||||
package_price numeric(10,2),
|
||||
amount numeric(10,2),
|
||||
billing_interval text,
|
||||
active_from timestamp with time zone DEFAULT now(),
|
||||
active_to timestamp with time zone,
|
||||
status text DEFAULT 'active'::text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
CONSTRAINT billing_contracts_interval_chk CHECK (((billing_interval IS NULL) OR (billing_interval = ANY (ARRAY['monthly'::text, 'weekly'::text])))),
|
||||
CONSTRAINT billing_contracts_sess_used_chk CHECK (((sessions_used IS NULL) OR (sessions_used >= 0))),
|
||||
CONSTRAINT billing_contracts_status_chk CHECK ((status = ANY (ARRAY['active'::text, 'completed'::text, 'cancelled'::text]))),
|
||||
CONSTRAINT billing_contracts_total_sess_chk CHECK (((total_sessions IS NULL) OR (total_sessions > 0))),
|
||||
CONSTRAINT billing_contracts_type_chk CHECK ((type = ANY (ARRAY['per_session'::text, 'package'::text, 'subscription'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.addon_credits (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
owner_id uuid,
|
||||
addon_type text NOT NULL,
|
||||
balance integer DEFAULT 0 NOT NULL,
|
||||
total_purchased integer DEFAULT 0 NOT NULL,
|
||||
total_consumed integer DEFAULT 0 NOT NULL,
|
||||
low_balance_threshold integer DEFAULT 10,
|
||||
low_balance_notified boolean DEFAULT false,
|
||||
daily_limit integer,
|
||||
hourly_limit integer,
|
||||
daily_used integer DEFAULT 0,
|
||||
hourly_used integer DEFAULT 0,
|
||||
daily_reset_at timestamp with time zone,
|
||||
hourly_reset_at timestamp with time zone,
|
||||
from_number_override text,
|
||||
expires_at timestamp with time zone,
|
||||
is_active boolean DEFAULT true,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
updated_at timestamp with time zone DEFAULT now()
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.addon_products (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
slug text NOT NULL,
|
||||
name text NOT NULL,
|
||||
description text,
|
||||
addon_type text NOT NULL,
|
||||
icon text DEFAULT 'pi pi-box'::text,
|
||||
credits_amount integer DEFAULT 0,
|
||||
price_cents integer DEFAULT 0 NOT NULL,
|
||||
currency text DEFAULT 'BRL'::text,
|
||||
is_active boolean DEFAULT true,
|
||||
is_visible boolean DEFAULT true,
|
||||
sort_order integer DEFAULT 0,
|
||||
metadata jsonb DEFAULT '{}'::jsonb,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
updated_at timestamp with time zone DEFAULT now(),
|
||||
deleted_at timestamp with time zone
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.addon_transactions (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
owner_id uuid,
|
||||
addon_type text NOT NULL,
|
||||
type text NOT NULL,
|
||||
amount integer NOT NULL,
|
||||
balance_before integer DEFAULT 0 NOT NULL,
|
||||
balance_after integer DEFAULT 0 NOT NULL,
|
||||
product_id uuid,
|
||||
queue_id uuid,
|
||||
description text,
|
||||
admin_user_id uuid,
|
||||
payment_method text,
|
||||
payment_reference text,
|
||||
price_cents integer,
|
||||
currency text DEFAULT 'BRL'::text,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
metadata jsonb DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.addon_transactions OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE addon_transactions; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE public.addon_transactions IS 'Histórico de todas as transações de créditos: compras, consumo, ajustes, reembolsos.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_bloqueios; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
|
||||
CREATE TABLE public.modules (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
key text NOT NULL,
|
||||
name text NOT NULL,
|
||||
description text,
|
||||
is_active boolean DEFAULT true NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.module_features (
|
||||
module_id uuid NOT NULL,
|
||||
feature_id uuid NOT NULL,
|
||||
enabled boolean DEFAULT true NOT NULL,
|
||||
limits jsonb,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.tenant_modules (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
module_id uuid NOT NULL,
|
||||
status text DEFAULT 'active'::text NOT NULL,
|
||||
settings jsonb,
|
||||
provider text DEFAULT 'manual'::text NOT NULL,
|
||||
provider_item_id text,
|
||||
installed_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.tenant_modules OWNER TO supabase_admin;
|
||||
204
database-novo/schema/04_tables/saas_admin.sql
Normal file
204
database-novo/schema/04_tables/saas_admin.sql
Normal file
@@ -0,0 +1,204 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Tables — SaaS Admin, FAQ, Docs, UI
|
||||
-- =============================================================================
|
||||
-- saas_docs, saas_doc_votos, saas_faq, saas_faq_itens,
|
||||
-- feriados, global_notices, login_carousel_slides, notice_dismissals,
|
||||
-- support_sessions
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE public.saas_doc_votos (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
doc_id uuid NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
util boolean NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.saas_doc_votos OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: saas_docs; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.saas_docs (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
titulo text NOT NULL,
|
||||
conteudo text DEFAULT ''::text NOT NULL,
|
||||
medias jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
tipo_acesso text DEFAULT 'usuario'::text NOT NULL,
|
||||
pagina_path text NOT NULL,
|
||||
docs_relacionados uuid[] DEFAULT '{}'::uuid[] NOT NULL,
|
||||
ativo boolean DEFAULT true NOT NULL,
|
||||
ordem integer DEFAULT 0 NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
categoria text,
|
||||
exibir_no_faq boolean DEFAULT false NOT NULL,
|
||||
votos_util integer DEFAULT 0 NOT NULL,
|
||||
votos_nao_util integer DEFAULT 0 NOT NULL,
|
||||
CONSTRAINT saas_docs_tipo_acesso_check CHECK ((tipo_acesso = ANY (ARRAY['admin'::text, 'usuario'::text])))
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.saas_docs OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: COLUMN saas_docs.categoria; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN public.saas_docs.categoria IS 'Agrupa docs no portal FAQ (ex: Conta, Agenda, Pagamentos)';
|
||||
|
||||
|
||||
--
|
||||
-- Name: COLUMN saas_docs.exibir_no_faq; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
COMMENT ON COLUMN public.saas_docs.exibir_no_faq IS 'Se true, a doc e seus itens FAQ aparecem no portal de FAQ';
|
||||
|
||||
|
||||
--
|
||||
-- Name: saas_faq; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.saas_faq (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
pergunta text NOT NULL,
|
||||
categoria text,
|
||||
publico boolean DEFAULT false NOT NULL,
|
||||
votos integer DEFAULT 0 NOT NULL,
|
||||
titulo text,
|
||||
conteudo text,
|
||||
tipo_acesso text DEFAULT 'usuario'::text NOT NULL,
|
||||
pagina_path text NOT NULL,
|
||||
pagina_label text,
|
||||
medias jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
faqs_relacionados uuid[] DEFAULT '{}'::uuid[] NOT NULL,
|
||||
ativo boolean DEFAULT true NOT NULL,
|
||||
ordem integer DEFAULT 0 NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.saas_faq OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: saas_faq_itens; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.saas_faq_itens (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
doc_id uuid NOT NULL,
|
||||
pergunta text NOT NULL,
|
||||
resposta text,
|
||||
ordem integer DEFAULT 0 NOT NULL,
|
||||
ativo boolean DEFAULT true NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.saas_faq_itens OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: TABLE saas_faq_itens; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
COMMENT ON TABLE public.saas_faq_itens IS 'Pares pergunta/resposta vinculados a um documento de ajuda';
|
||||
|
||||
|
||||
--
|
||||
-- Name: services; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
|
||||
CREATE TABLE public.feriados (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid,
|
||||
owner_id uuid,
|
||||
tipo text DEFAULT 'municipal'::text NOT NULL,
|
||||
nome text NOT NULL,
|
||||
data date NOT NULL,
|
||||
cidade text,
|
||||
estado text,
|
||||
observacao text,
|
||||
bloqueia_sessoes boolean DEFAULT false NOT NULL,
|
||||
criado_em timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT feriados_tipo_check CHECK ((tipo = ANY (ARRAY['municipal'::text, 'personalizado'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.global_notices (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
title text,
|
||||
message text DEFAULT ''::text NOT NULL,
|
||||
variant text DEFAULT 'info'::text NOT NULL,
|
||||
roles text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
contexts text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
starts_at timestamp with time zone,
|
||||
ends_at timestamp with time zone,
|
||||
is_active boolean DEFAULT true NOT NULL,
|
||||
priority integer DEFAULT 0 NOT NULL,
|
||||
dismissible boolean DEFAULT true NOT NULL,
|
||||
persist_dismiss boolean DEFAULT true NOT NULL,
|
||||
dismiss_scope text DEFAULT 'device'::text NOT NULL,
|
||||
show_once boolean DEFAULT false NOT NULL,
|
||||
max_views integer,
|
||||
cooldown_minutes integer,
|
||||
version integer DEFAULT 1 NOT NULL,
|
||||
action_type text DEFAULT 'none'::text NOT NULL,
|
||||
action_label text,
|
||||
action_url text,
|
||||
action_route text,
|
||||
views_count integer DEFAULT 0 NOT NULL,
|
||||
clicks_count integer DEFAULT 0 NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
created_by uuid,
|
||||
content_align text DEFAULT 'left'::text NOT NULL,
|
||||
link_target text DEFAULT '_blank'::text NOT NULL,
|
||||
CONSTRAINT global_notices_action_type_check CHECK ((action_type = ANY (ARRAY['none'::text, 'internal'::text, 'external'::text]))),
|
||||
CONSTRAINT global_notices_content_align_check CHECK ((content_align = ANY (ARRAY['left'::text, 'center'::text, 'right'::text, 'justify'::text]))),
|
||||
CONSTRAINT global_notices_dismiss_scope_check CHECK ((dismiss_scope = ANY (ARRAY['session'::text, 'device'::text, 'user'::text]))),
|
||||
CONSTRAINT global_notices_link_target_check CHECK ((link_target = ANY (ARRAY['_blank'::text, '_self'::text, '_parent'::text, '_top'::text]))),
|
||||
CONSTRAINT global_notices_variant_check CHECK ((variant = ANY (ARRAY['info'::text, 'success'::text, 'warning'::text, 'error'::text])))
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.login_carousel_slides (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
title text NOT NULL,
|
||||
body text NOT NULL,
|
||||
icon text DEFAULT 'pi-star'::text NOT NULL,
|
||||
ordem integer DEFAULT 0 NOT NULL,
|
||||
ativo boolean DEFAULT true NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
updated_at timestamp with time zone DEFAULT now()
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.notice_dismissals (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
notice_id uuid NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
version integer DEFAULT 1 NOT NULL,
|
||||
dismissed_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE public.support_sessions (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
admin_id uuid NOT NULL,
|
||||
token text DEFAULT encode(extensions.gen_random_bytes(32), 'hex'::text) NOT NULL,
|
||||
expires_at timestamp with time zone DEFAULT (now() + '01:00:00'::interval) NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
596
database-novo/schema/05_views/views.sql
Normal file
596
database-novo/schema/05_views/views.sql
Normal file
@@ -0,0 +1,596 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Views
|
||||
-- =============================================================================
|
||||
-- current_tenant_id, owner_feature_entitlements, subscription_intents,
|
||||
-- v_auth_users_public, v_cashflow_projection, v_commitment_totals,
|
||||
-- v_patient_groups_with_counts, v_plan_active_prices, v_public_pricing,
|
||||
-- v_subscription_feature_mismatch, v_subscription_health, v_subscription_health_v2,
|
||||
-- v_tag_patient_counts, v_tenant_active_subscription, v_tenant_entitlements,
|
||||
-- v_tenant_entitlements_full, v_tenant_entitlements_json,
|
||||
-- v_tenant_feature_exceptions, v_tenant_feature_mismatch,
|
||||
-- v_tenant_members_with_profiles, v_tenant_people, v_tenant_staff,
|
||||
-- v_user_active_subscription, v_user_entitlements
|
||||
-- =============================================================================
|
||||
|
||||
CREATE VIEW public.current_tenant_id AS
|
||||
SELECT current_setting('request.jwt.claim.tenant_id'::text, true) AS current_setting;
|
||||
|
||||
|
||||
ALTER VIEW public.current_tenant_id OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: determined_commitment_fields; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
|
||||
CREATE VIEW public.owner_feature_entitlements AS
|
||||
WITH base AS (
|
||||
SELECT s.user_id AS owner_id,
|
||||
f.key AS feature_key,
|
||||
pf.limits,
|
||||
'plan'::text AS source
|
||||
FROM ((public.subscriptions s
|
||||
JOIN public.plan_features pf ON (((pf.plan_id = s.plan_id) AND (pf.enabled = true))))
|
||||
JOIN public.features f ON ((f.id = pf.feature_id)))
|
||||
WHERE ((s.status = 'active'::text) AND (s.user_id IS NOT NULL))
|
||||
UNION ALL
|
||||
SELECT tm.owner_id,
|
||||
f.key AS feature_key,
|
||||
mf.limits,
|
||||
'module'::text AS source
|
||||
FROM (((public.tenant_modules tm
|
||||
JOIN public.modules m ON (((m.id = tm.module_id) AND (m.is_active = true))))
|
||||
JOIN public.module_features mf ON (((mf.module_id = m.id) AND (mf.enabled = true))))
|
||||
JOIN public.features f ON ((f.id = mf.feature_id)))
|
||||
WHERE ((tm.status = 'active'::text) AND (tm.owner_id IS NOT NULL))
|
||||
)
|
||||
SELECT owner_id,
|
||||
feature_key,
|
||||
array_agg(DISTINCT source) AS sources,
|
||||
jsonb_agg(limits) FILTER (WHERE (limits IS NOT NULL)) AS limits_list
|
||||
FROM base
|
||||
GROUP BY owner_id, feature_key;
|
||||
|
||||
|
||||
ALTER VIEW public.owner_feature_entitlements OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: owner_users; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
|
||||
CREATE VIEW public.subscription_intents AS
|
||||
SELECT t.id,
|
||||
t.user_id,
|
||||
t.created_by_user_id,
|
||||
t.email,
|
||||
t.plan_id,
|
||||
t.plan_key,
|
||||
t."interval",
|
||||
t.amount_cents,
|
||||
t.currency,
|
||||
t.status,
|
||||
t.source,
|
||||
t.notes,
|
||||
t.created_at,
|
||||
t.paid_at,
|
||||
t.tenant_id,
|
||||
t.subscription_id,
|
||||
'clinic'::text AS plan_target
|
||||
FROM public.subscription_intents_tenant t
|
||||
UNION ALL
|
||||
SELECT p.id,
|
||||
p.user_id,
|
||||
p.created_by_user_id,
|
||||
p.email,
|
||||
p.plan_id,
|
||||
p.plan_key,
|
||||
p."interval",
|
||||
p.amount_cents,
|
||||
p.currency,
|
||||
p.status,
|
||||
p.source,
|
||||
p.notes,
|
||||
p.created_at,
|
||||
p.paid_at,
|
||||
NULL::uuid AS tenant_id,
|
||||
p.subscription_id,
|
||||
'therapist'::text AS plan_target
|
||||
FROM public.subscription_intents_personal p;
|
||||
|
||||
|
||||
ALTER VIEW public.subscription_intents OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: subscription_intents_legacy; Type: TABLE; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
|
||||
CREATE VIEW public.v_auth_users_public AS
|
||||
SELECT id AS user_id,
|
||||
email,
|
||||
created_at,
|
||||
last_sign_in_at
|
||||
FROM auth.users u;
|
||||
|
||||
|
||||
ALTER VIEW public.v_auth_users_public OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_cashflow_projection; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_cashflow_projection WITH (security_invoker='on') AS
|
||||
SELECT gs.mes,
|
||||
to_char(gs.mes, 'YYYY-MM'::text) AS mes_label,
|
||||
COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'receita'::public.financial_record_type) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])))), (0)::numeric) AS receitas_projetadas,
|
||||
COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'despesa'::public.financial_record_type) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])))), (0)::numeric) AS despesas_projetadas,
|
||||
COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'receita'::public.financial_record_type) AND (fr.status = 'pending'::text))), (0)::numeric) AS receitas_pendentes,
|
||||
COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'receita'::public.financial_record_type) AND (fr.status = 'overdue'::text))), (0)::numeric) AS receitas_vencidas,
|
||||
COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'despesa'::public.financial_record_type) AND (fr.status = 'pending'::text))), (0)::numeric) AS despesas_pendentes,
|
||||
COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'despesa'::public.financial_record_type) AND (fr.status = 'overdue'::text))), (0)::numeric) AS despesas_vencidas,
|
||||
(COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'receita'::public.financial_record_type) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])))), (0)::numeric) - COALESCE(sum(fr.final_amount) FILTER (WHERE ((fr.type = 'despesa'::public.financial_record_type) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])))), (0)::numeric)) AS saldo_projetado,
|
||||
count(fr.id) FILTER (WHERE (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text]))) AS count_registros
|
||||
FROM (generate_series(((date_trunc('month'::text, (CURRENT_DATE)::timestamp with time zone))::date)::timestamp with time zone, (((date_trunc('month'::text, (CURRENT_DATE)::timestamp with time zone) + '5 mons'::interval))::date)::timestamp with time zone, '1 mon'::interval) gs(mes)
|
||||
LEFT JOIN public.financial_records fr ON (((fr.deleted_at IS NULL) AND (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])) AND ((date_trunc('month'::text, (fr.due_date)::timestamp with time zone))::date = gs.mes))))
|
||||
GROUP BY gs.mes
|
||||
ORDER BY gs.mes;
|
||||
|
||||
|
||||
ALTER VIEW public.v_cashflow_projection OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: VIEW v_cashflow_projection; Type: COMMENT; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
COMMENT ON VIEW public.v_cashflow_projection IS 'Fluxo de caixa projetado: próximos 6 meses com totais de pending+overdue por due_date. Usa security_invoker=on — filtra automaticamente pelo auth.uid() via RLS de financial_records.';
|
||||
|
||||
|
||||
--
|
||||
-- Name: v_commitment_totals; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_commitment_totals AS
|
||||
SELECT c.tenant_id,
|
||||
c.id AS commitment_id,
|
||||
(COALESCE(sum(l.minutes), (0)::bigint))::integer AS total_minutes
|
||||
FROM (public.determined_commitments c
|
||||
LEFT JOIN public.commitment_time_logs l ON ((l.commitment_id = c.id)))
|
||||
GROUP BY c.tenant_id, c.id;
|
||||
|
||||
|
||||
ALTER VIEW public.v_commitment_totals OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_patient_groups_with_counts; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_patient_groups_with_counts AS
|
||||
SELECT pg.id,
|
||||
pg.nome,
|
||||
pg.cor,
|
||||
pg.owner_id,
|
||||
pg.is_system,
|
||||
pg.is_active,
|
||||
pg.created_at,
|
||||
pg.updated_at,
|
||||
(COALESCE(count(pgp.patient_id), (0)::bigint))::integer AS patients_count
|
||||
FROM (public.patient_groups pg
|
||||
LEFT JOIN public.patient_group_patient pgp ON ((pgp.patient_group_id = pg.id)))
|
||||
GROUP BY pg.id, pg.nome, pg.cor, pg.owner_id, pg.is_system, pg.is_active, pg.created_at, pg.updated_at;
|
||||
|
||||
|
||||
ALTER VIEW public.v_patient_groups_with_counts OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_plan_active_prices; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_plan_active_prices AS
|
||||
SELECT plan_id,
|
||||
max(
|
||||
CASE
|
||||
WHEN (("interval" = 'month'::text) AND is_active) THEN amount_cents
|
||||
ELSE NULL::integer
|
||||
END) AS monthly_cents,
|
||||
max(
|
||||
CASE
|
||||
WHEN (("interval" = 'year'::text) AND is_active) THEN amount_cents
|
||||
ELSE NULL::integer
|
||||
END) AS yearly_cents,
|
||||
max(
|
||||
CASE
|
||||
WHEN (("interval" = 'month'::text) AND is_active) THEN currency
|
||||
ELSE NULL::text
|
||||
END) AS monthly_currency,
|
||||
max(
|
||||
CASE
|
||||
WHEN (("interval" = 'year'::text) AND is_active) THEN currency
|
||||
ELSE NULL::text
|
||||
END) AS yearly_currency
|
||||
FROM public.plan_prices
|
||||
GROUP BY plan_id;
|
||||
|
||||
|
||||
ALTER VIEW public.v_plan_active_prices OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_public_pricing; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_public_pricing AS
|
||||
SELECT p.id AS plan_id,
|
||||
p.key AS plan_key,
|
||||
p.name AS plan_name,
|
||||
COALESCE(pp.public_name, ''::text) AS public_name,
|
||||
COALESCE(pp.public_description, ''::text) AS public_description,
|
||||
pp.badge,
|
||||
COALESCE(pp.is_featured, false) AS is_featured,
|
||||
COALESCE(pp.is_visible, true) AS is_visible,
|
||||
COALESCE(pp.sort_order, 0) AS sort_order,
|
||||
ap.monthly_cents,
|
||||
ap.yearly_cents,
|
||||
ap.monthly_currency,
|
||||
ap.yearly_currency,
|
||||
COALESCE(( SELECT jsonb_agg(jsonb_build_object('id', b.id, 'text', b.text, 'highlight', b.highlight, 'sort_order', b.sort_order) ORDER BY b.sort_order, b.created_at) AS jsonb_agg
|
||||
FROM public.plan_public_bullets b
|
||||
WHERE (b.plan_id = p.id)), '[]'::jsonb) AS bullets,
|
||||
p.target AS plan_target
|
||||
FROM ((public.plans p
|
||||
LEFT JOIN public.plan_public pp ON ((pp.plan_id = p.id)))
|
||||
LEFT JOIN public.v_plan_active_prices ap ON ((ap.plan_id = p.id)))
|
||||
ORDER BY COALESCE(pp.sort_order, 0), p.key;
|
||||
|
||||
|
||||
ALTER VIEW public.v_public_pricing OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_subscription_feature_mismatch; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_subscription_feature_mismatch AS
|
||||
WITH expected AS (
|
||||
SELECT s.user_id AS owner_id,
|
||||
f.key AS feature_key
|
||||
FROM ((public.subscriptions s
|
||||
JOIN public.plan_features pf ON (((pf.plan_id = s.plan_id) AND (pf.enabled = true))))
|
||||
JOIN public.features f ON ((f.id = pf.feature_id)))
|
||||
WHERE ((s.status = 'active'::text) AND (s.tenant_id IS NULL) AND (s.user_id IS NOT NULL))
|
||||
), actual AS (
|
||||
SELECT e.owner_id,
|
||||
e.feature_key
|
||||
FROM public.owner_feature_entitlements e
|
||||
)
|
||||
SELECT COALESCE(expected.owner_id, actual.owner_id) AS owner_id,
|
||||
COALESCE(expected.feature_key, actual.feature_key) AS feature_key,
|
||||
CASE
|
||||
WHEN ((expected.feature_key IS NOT NULL) AND (actual.feature_key IS NULL)) THEN 'missing_entitlement'::text
|
||||
WHEN ((expected.feature_key IS NULL) AND (actual.feature_key IS NOT NULL)) THEN 'unexpected_entitlement'::text
|
||||
ELSE NULL::text
|
||||
END AS mismatch_type
|
||||
FROM (expected
|
||||
FULL JOIN actual ON (((expected.owner_id = actual.owner_id) AND (expected.feature_key = actual.feature_key))))
|
||||
WHERE ((expected.feature_key IS NULL) OR (actual.feature_key IS NULL));
|
||||
|
||||
|
||||
ALTER VIEW public.v_subscription_feature_mismatch OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_subscription_health; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_subscription_health AS
|
||||
SELECT s.id AS subscription_id,
|
||||
s.user_id AS owner_id,
|
||||
s.status,
|
||||
s.plan_id,
|
||||
p.key AS plan_key,
|
||||
s.current_period_start,
|
||||
s.current_period_end,
|
||||
s.updated_at,
|
||||
CASE
|
||||
WHEN (s.plan_id IS NULL) THEN 'missing_plan'::text
|
||||
WHEN (p.id IS NULL) THEN 'invalid_plan'::text
|
||||
WHEN ((s.status = 'active'::text) AND (s.current_period_end IS NOT NULL) AND (s.current_period_end < now())) THEN 'expired_but_active'::text
|
||||
WHEN ((s.status = 'canceled'::text) AND (s.current_period_end > now())) THEN 'canceled_but_still_in_period'::text
|
||||
ELSE 'ok'::text
|
||||
END AS health_status,
|
||||
CASE
|
||||
WHEN (s.tenant_id IS NOT NULL) THEN 'clinic'::text
|
||||
ELSE 'therapist'::text
|
||||
END AS owner_type,
|
||||
COALESCE(s.tenant_id, s.user_id) AS owner_ref
|
||||
FROM (public.subscriptions s
|
||||
LEFT JOIN public.plans p ON ((p.id = s.plan_id)));
|
||||
|
||||
|
||||
ALTER VIEW public.v_subscription_health OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_subscription_health_v2; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_subscription_health_v2 AS
|
||||
SELECT s.id AS subscription_id,
|
||||
s.user_id AS owner_id,
|
||||
CASE
|
||||
WHEN (s.tenant_id IS NOT NULL) THEN 'clinic'::text
|
||||
ELSE 'therapist'::text
|
||||
END AS owner_type,
|
||||
COALESCE(s.tenant_id, s.user_id) AS owner_ref,
|
||||
s.status,
|
||||
s.plan_id,
|
||||
p.key AS plan_key,
|
||||
s.current_period_start,
|
||||
s.current_period_end,
|
||||
s.updated_at,
|
||||
CASE
|
||||
WHEN (s.plan_id IS NULL) THEN 'missing_plan'::text
|
||||
WHEN (p.id IS NULL) THEN 'invalid_plan'::text
|
||||
WHEN ((s.status = 'active'::text) AND (s.current_period_end IS NOT NULL) AND (s.current_period_end < now())) THEN 'expired_but_active'::text
|
||||
WHEN ((s.status = 'canceled'::text) AND (s.current_period_end > now())) THEN 'canceled_but_still_in_period'::text
|
||||
ELSE 'ok'::text
|
||||
END AS health_status
|
||||
FROM (public.subscriptions s
|
||||
LEFT JOIN public.plans p ON ((p.id = s.plan_id)));
|
||||
|
||||
|
||||
ALTER VIEW public.v_subscription_health_v2 OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_tag_patient_counts; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_tag_patient_counts AS
|
||||
SELECT t.id,
|
||||
t.owner_id,
|
||||
t.nome,
|
||||
t.cor,
|
||||
t.is_padrao,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
(COALESCE(count(ppt.patient_id), (0)::bigint))::integer AS pacientes_count,
|
||||
(COALESCE(count(ppt.patient_id), (0)::bigint))::integer AS patient_count
|
||||
FROM (public.patient_tags t
|
||||
LEFT JOIN public.patient_patient_tag ppt ON (((ppt.tag_id = t.id) AND (ppt.owner_id = t.owner_id))))
|
||||
GROUP BY t.id, t.owner_id, t.nome, t.cor, t.is_padrao, t.created_at, t.updated_at;
|
||||
|
||||
|
||||
ALTER VIEW public.v_tag_patient_counts OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_tenant_active_subscription; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_tenant_active_subscription AS
|
||||
SELECT DISTINCT ON (tenant_id) tenant_id,
|
||||
plan_id,
|
||||
plan_key,
|
||||
"interval",
|
||||
status,
|
||||
current_period_start,
|
||||
current_period_end,
|
||||
created_at
|
||||
FROM public.subscriptions s
|
||||
WHERE ((tenant_id IS NOT NULL) AND (status = 'active'::text) AND ((current_period_end IS NULL) OR (current_period_end > now())))
|
||||
ORDER BY tenant_id, created_at DESC;
|
||||
|
||||
|
||||
ALTER VIEW public.v_tenant_active_subscription OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_tenant_entitlements; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_tenant_entitlements AS
|
||||
SELECT a.tenant_id,
|
||||
f.key AS feature_key,
|
||||
true AS allowed
|
||||
FROM ((public.v_tenant_active_subscription a
|
||||
JOIN public.plan_features pf ON (((pf.plan_id = a.plan_id) AND (pf.enabled = true))))
|
||||
JOIN public.features f ON ((f.id = pf.feature_id)));
|
||||
|
||||
|
||||
ALTER VIEW public.v_tenant_entitlements OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_tenant_entitlements_full; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_tenant_entitlements_full AS
|
||||
SELECT a.tenant_id,
|
||||
f.key AS feature_key,
|
||||
(pf.enabled = true) AS allowed,
|
||||
pf.limits,
|
||||
a.plan_id,
|
||||
p.key AS plan_key
|
||||
FROM (((public.v_tenant_active_subscription a
|
||||
JOIN public.plan_features pf ON ((pf.plan_id = a.plan_id)))
|
||||
JOIN public.features f ON ((f.id = pf.feature_id)))
|
||||
JOIN public.plans p ON ((p.id = a.plan_id)));
|
||||
|
||||
|
||||
ALTER VIEW public.v_tenant_entitlements_full OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_tenant_entitlements_json; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_tenant_entitlements_json AS
|
||||
SELECT tenant_id,
|
||||
max(plan_key) AS plan_key,
|
||||
jsonb_object_agg(feature_key, jsonb_build_object('allowed', allowed, 'limits', COALESCE(limits, '{}'::jsonb)) ORDER BY feature_key) AS entitlements
|
||||
FROM public.v_tenant_entitlements_full
|
||||
GROUP BY tenant_id;
|
||||
|
||||
|
||||
ALTER VIEW public.v_tenant_entitlements_json OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_tenant_feature_exceptions; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_tenant_feature_exceptions AS
|
||||
SELECT tf.tenant_id,
|
||||
a.plan_key,
|
||||
tf.feature_key,
|
||||
'commercial_exception'::text AS exception_type
|
||||
FROM ((public.tenant_features tf
|
||||
JOIN public.v_tenant_active_subscription a ON ((a.tenant_id = tf.tenant_id)))
|
||||
LEFT JOIN public.v_tenant_entitlements_full v ON (((v.tenant_id = tf.tenant_id) AND (v.feature_key = tf.feature_key))))
|
||||
WHERE ((tf.enabled = true) AND (COALESCE(v.allowed, false) = false));
|
||||
|
||||
|
||||
ALTER VIEW public.v_tenant_feature_exceptions OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_tenant_feature_mismatch; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_tenant_feature_mismatch AS
|
||||
WITH plan_allowed AS (
|
||||
SELECT v.tenant_id,
|
||||
v.feature_key,
|
||||
v.allowed
|
||||
FROM public.v_tenant_entitlements_full v
|
||||
), overrides AS (
|
||||
SELECT tf.tenant_id,
|
||||
tf.feature_key,
|
||||
tf.enabled
|
||||
FROM public.tenant_features tf
|
||||
)
|
||||
SELECT o.tenant_id,
|
||||
o.feature_key,
|
||||
CASE
|
||||
WHEN ((o.enabled = true) AND (COALESCE(p.allowed, false) = false)) THEN 'unexpected_override'::text
|
||||
ELSE NULL::text
|
||||
END AS mismatch_type
|
||||
FROM (overrides o
|
||||
LEFT JOIN plan_allowed p ON (((p.tenant_id = o.tenant_id) AND (p.feature_key = o.feature_key))))
|
||||
WHERE ((o.enabled = true) AND (COALESCE(p.allowed, false) = false));
|
||||
|
||||
|
||||
ALTER VIEW public.v_tenant_feature_mismatch OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_tenant_members_with_profiles; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_tenant_members_with_profiles AS
|
||||
SELECT tm.id AS tenant_member_id,
|
||||
tm.tenant_id,
|
||||
tm.user_id,
|
||||
tm.role,
|
||||
tm.status,
|
||||
tm.created_at,
|
||||
p.full_name,
|
||||
au.email
|
||||
FROM ((public.tenant_members tm
|
||||
LEFT JOIN public.profiles p ON ((p.id = tm.user_id)))
|
||||
LEFT JOIN auth.users au ON ((au.id = tm.user_id)));
|
||||
|
||||
|
||||
ALTER VIEW public.v_tenant_members_with_profiles OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_tenant_people; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_tenant_people AS
|
||||
SELECT 'member'::text AS type,
|
||||
m.tenant_id,
|
||||
m.user_id,
|
||||
u.email,
|
||||
m.role,
|
||||
m.status,
|
||||
NULL::uuid AS invite_token,
|
||||
NULL::timestamp with time zone AS expires_at
|
||||
FROM (public.tenant_members m
|
||||
JOIN auth.users u ON ((u.id = m.user_id)))
|
||||
UNION ALL
|
||||
SELECT 'invite'::text AS type,
|
||||
i.tenant_id,
|
||||
NULL::uuid AS user_id,
|
||||
i.email,
|
||||
i.role,
|
||||
'invited'::text AS status,
|
||||
i.token AS invite_token,
|
||||
i.expires_at
|
||||
FROM public.tenant_invites i
|
||||
WHERE ((i.accepted_at IS NULL) AND (i.revoked_at IS NULL));
|
||||
|
||||
|
||||
ALTER VIEW public.v_tenant_people OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_tenant_staff; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_tenant_staff AS
|
||||
SELECT ('m_'::text || (tm.id)::text) AS row_id,
|
||||
tm.tenant_id,
|
||||
tm.user_id,
|
||||
tm.role,
|
||||
tm.status,
|
||||
tm.created_at,
|
||||
p.full_name,
|
||||
au.email,
|
||||
NULL::uuid AS invite_token
|
||||
FROM ((public.tenant_members tm
|
||||
LEFT JOIN public.profiles p ON ((p.id = tm.user_id)))
|
||||
LEFT JOIN auth.users au ON ((au.id = tm.user_id)))
|
||||
UNION ALL
|
||||
SELECT ('i_'::text || (ti.id)::text) AS row_id,
|
||||
ti.tenant_id,
|
||||
NULL::uuid AS user_id,
|
||||
ti.role,
|
||||
'invited'::text AS status,
|
||||
ti.created_at,
|
||||
NULL::text AS full_name,
|
||||
ti.email,
|
||||
ti.token AS invite_token
|
||||
FROM public.tenant_invites ti
|
||||
WHERE ((ti.accepted_at IS NULL) AND (ti.revoked_at IS NULL) AND (ti.expires_at > now()));
|
||||
|
||||
|
||||
ALTER VIEW public.v_tenant_staff OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_user_active_subscription; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_user_active_subscription AS
|
||||
SELECT DISTINCT ON (user_id) user_id,
|
||||
plan_id,
|
||||
plan_key,
|
||||
"interval",
|
||||
status,
|
||||
current_period_start,
|
||||
current_period_end,
|
||||
created_at
|
||||
FROM public.subscriptions s
|
||||
WHERE ((tenant_id IS NULL) AND (user_id IS NOT NULL) AND (status = 'active'::text) AND ((current_period_end IS NULL) OR (current_period_end > now())))
|
||||
ORDER BY user_id, created_at DESC;
|
||||
|
||||
|
||||
ALTER VIEW public.v_user_active_subscription OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: v_user_entitlements; Type: VIEW; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE VIEW public.v_user_entitlements AS
|
||||
SELECT a.user_id,
|
||||
f.key AS feature_key,
|
||||
true AS allowed
|
||||
FROM ((public.v_user_active_subscription a
|
||||
JOIN public.plan_features pf ON (((pf.plan_id = a.plan_id) AND (pf.enabled = true))))
|
||||
JOIN public.features f ON ((f.id = pf.feature_id)));
|
||||
|
||||
|
||||
ALTER VIEW public.v_user_entitlements OWNER TO supabase_admin;
|
||||
|
||||
--
|
||||
-- Name: messages; Type: TABLE; Schema: realtime; Owner: supabase_realtime_admin
|
||||
--
|
||||
|
||||
2049
database-novo/schema/06_indexes/indexes.sql
Normal file
2049
database-novo/schema/06_indexes/indexes.sql
Normal file
File diff suppressed because it is too large
Load Diff
2629
database-novo/schema/07_foreign_keys/constraints.sql
Normal file
2629
database-novo/schema/07_foreign_keys/constraints.sql
Normal file
File diff suppressed because it is too large
Load Diff
481
database-novo/schema/08_triggers/triggers.sql
Normal file
481
database-novo/schema/08_triggers/triggers.sql
Normal file
@@ -0,0 +1,481 @@
|
||||
-- =============================================================================
|
||||
-- AgenciaPsi — Triggers
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
||||
|
||||
|
||||
--
|
||||
-- Name: users trg_seed_patient_groups; Type: TRIGGER; Schema: auth; Owner: supabase_auth_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_seed_patient_groups AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.on_new_user_seed_patient_groups();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_bloqueios agenda_bloqueios_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER agenda_bloqueios_updated_at BEFORE UPDATE ON public.agenda_bloqueios FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agendador_configuracoes agendador_slug_trigger; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER agendador_slug_trigger BEFORE INSERT OR UPDATE ON public.agendador_configuracoes FOR EACH ROW EXECUTE FUNCTION public.agendador_gerar_slug();
|
||||
|
||||
|
||||
--
|
||||
-- Name: tenant_members prevent_saas_membership_trigger; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER prevent_saas_membership_trigger BEFORE INSERT ON public.tenant_members FOR EACH ROW EXECUTE FUNCTION public.prevent_saas_membership();
|
||||
|
||||
|
||||
--
|
||||
-- Name: insurance_plan_services set_insurance_plan_services_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER set_insurance_plan_services_updated_at BEFORE UPDATE ON public.insurance_plan_services FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_settings t_user_settings_set_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER t_user_settings_set_updated_at BEFORE UPDATE ON public.user_settings FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_configuracoes tg_agenda_configuracoes_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER tg_agenda_configuracoes_updated_at BEFORE UPDATE ON public.agenda_configuracoes FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_eventos tg_agenda_eventos_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER tg_agenda_eventos_updated_at BEFORE UPDATE ON public.agenda_eventos FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_excecoes tg_agenda_excecoes_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER tg_agenda_excecoes_updated_at BEFORE UPDATE ON public.agenda_excecoes FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_regras_semanais tg_agenda_regras_semanais_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER tg_agenda_regras_semanais_updated_at BEFORE UPDATE ON public.agenda_regras_semanais FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: recurrence_rules tg_recurrence_rules_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER tg_recurrence_rules_updated_at BEFORE UPDATE ON public.recurrence_rules FOR EACH ROW EXECUTE FUNCTION public.set_updated_at_recurrence();
|
||||
|
||||
|
||||
--
|
||||
-- Name: plan_public tr_plan_public_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER tr_plan_public_updated_at BEFORE UPDATE ON public.plan_public FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: profiles trg_account_type_immutable; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_account_type_immutable BEFORE UPDATE OF account_type ON public.profiles FOR EACH ROW EXECUTE FUNCTION public.guard_account_type_immutable();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_configuracoes trg_agenda_cfg_sync; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_agenda_cfg_sync BEFORE INSERT OR UPDATE ON public.agenda_configuracoes FOR EACH ROW EXECUTE FUNCTION public.agenda_cfg_sync();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_eventos trg_agenda_eventos_busy_mirror_del; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_agenda_eventos_busy_mirror_del AFTER DELETE ON public.agenda_eventos FOR EACH ROW WHEN (((old.mirror_of_event_id IS NULL) AND (old.tenant_id = old.owner_id))) EXECUTE FUNCTION public.sync_busy_mirror_agenda_eventos();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_eventos trg_agenda_eventos_busy_mirror_ins; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_agenda_eventos_busy_mirror_ins AFTER INSERT ON public.agenda_eventos FOR EACH ROW WHEN (((new.mirror_of_event_id IS NULL) AND (new.tenant_id = new.owner_id) AND (new.visibility_scope = ANY (ARRAY['busy_only'::text, 'private'::text])))) EXECUTE FUNCTION public.sync_busy_mirror_agenda_eventos();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_eventos trg_agenda_eventos_busy_mirror_upd; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_agenda_eventos_busy_mirror_upd AFTER UPDATE ON public.agenda_eventos FOR EACH ROW WHEN (((new.mirror_of_event_id IS NULL) AND (new.tenant_id = new.owner_id) AND ((new.visibility_scope IS DISTINCT FROM old.visibility_scope) OR (new.inicio_em IS DISTINCT FROM old.inicio_em) OR (new.fim_em IS DISTINCT FROM old.fim_em) OR (new.owner_id IS DISTINCT FROM old.owner_id) OR (new.tenant_id IS DISTINCT FROM old.tenant_id)))) EXECUTE FUNCTION public.sync_busy_mirror_agenda_eventos();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_regras_semanais trg_agenda_regras_semanais_no_overlap; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_agenda_regras_semanais_no_overlap BEFORE INSERT OR UPDATE ON public.agenda_regras_semanais FOR EACH ROW EXECUTE FUNCTION public.fn_agenda_regras_semanais_no_overlap();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_eventos trg_auto_financial_from_session; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_auto_financial_from_session AFTER UPDATE OF status ON public.agenda_eventos FOR EACH ROW EXECUTE FUNCTION public.auto_create_financial_record_from_session();
|
||||
|
||||
|
||||
--
|
||||
-- Name: notification_preferences trg_cancel_notifs_on_opt_out; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_cancel_notifs_on_opt_out AFTER UPDATE ON public.notification_preferences FOR EACH ROW EXECUTE FUNCTION public.cancel_notifications_on_opt_out();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_eventos trg_cancel_notifs_on_session_cancel; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_cancel_notifs_on_session_cancel AFTER UPDATE ON public.agenda_eventos FOR EACH ROW WHEN ((new.status IS DISTINCT FROM old.status)) EXECUTE FUNCTION public.cancel_notifications_on_session_cancel();
|
||||
|
||||
|
||||
--
|
||||
-- Name: company_profiles trg_company_profiles_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_company_profiles_updated_at BEFORE UPDATE ON public.company_profiles FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: determined_commitment_fields trg_determined_commitment_fields_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_determined_commitment_fields_updated_at BEFORE UPDATE ON public.determined_commitment_fields FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: determined_commitments trg_determined_commitments_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_determined_commitments_updated_at BEFORE UPDATE ON public.determined_commitments FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: email_layout_config trg_email_layout_config_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_email_layout_config_updated_at BEFORE UPDATE ON public.email_layout_config FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: email_templates_global trg_email_templates_global_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_email_templates_global_updated_at BEFORE UPDATE ON public.email_templates_global FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: email_templates_tenant trg_email_templates_tenant_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_email_templates_tenant_updated_at BEFORE UPDATE ON public.email_templates_tenant FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: financial_exceptions trg_financial_exceptions_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_financial_exceptions_updated_at BEFORE UPDATE ON public.financial_exceptions FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: financial_records trg_financial_records_auto_overdue; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_financial_records_auto_overdue BEFORE UPDATE ON public.financial_records FOR EACH ROW EXECUTE FUNCTION public.trg_fn_financial_records_auto_overdue();
|
||||
|
||||
|
||||
--
|
||||
-- Name: financial_records trg_financial_records_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_financial_records_updated_at BEFORE UPDATE ON public.financial_records FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: global_notices trg_global_notices_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_global_notices_updated_at BEFORE UPDATE ON public.global_notices FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: insurance_plans trg_insurance_plans_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_insurance_plans_updated_at BEFORE UPDATE ON public.insurance_plans FOR EACH ROW EXECUTE FUNCTION public.set_insurance_plans_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: plans trg_no_change_core_plan_key; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_no_change_core_plan_key BEFORE UPDATE ON public.plans FOR EACH ROW EXECUTE FUNCTION public.guard_no_change_core_plan_key();
|
||||
|
||||
|
||||
--
|
||||
-- Name: plans trg_no_change_plan_target; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_no_change_plan_target BEFORE UPDATE ON public.plans FOR EACH ROW EXECUTE FUNCTION public.guard_no_change_plan_target();
|
||||
|
||||
|
||||
--
|
||||
-- Name: plans trg_no_delete_core_plans; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_no_delete_core_plans BEFORE DELETE ON public.plans FOR EACH ROW EXECUTE FUNCTION public.guard_no_delete_core_plans();
|
||||
|
||||
|
||||
--
|
||||
-- Name: notification_channels trg_notification_channels_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_notification_channels_updated_at BEFORE UPDATE ON public.notification_channels FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: notification_logs trg_notification_logs_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_notification_logs_updated_at BEFORE UPDATE ON public.notification_logs FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: notification_preferences trg_notification_preferences_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_notification_preferences_updated_at BEFORE UPDATE ON public.notification_preferences FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: notification_queue trg_notification_queue_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_notification_queue_updated_at BEFORE UPDATE ON public.notification_queue FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: notification_schedules trg_notification_schedules_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_notification_schedules_updated_at BEFORE UPDATE ON public.notification_schedules FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: notification_templates trg_notification_templates_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_notification_templates_updated_at BEFORE UPDATE ON public.notification_templates FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: patient_intake_requests trg_notify_on_intake; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_notify_on_intake AFTER INSERT ON public.patient_intake_requests FOR EACH ROW EXECUTE FUNCTION public.notify_on_intake();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agendador_solicitacoes trg_notify_on_scheduling; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_notify_on_scheduling AFTER INSERT ON public.agendador_solicitacoes FOR EACH ROW EXECUTE FUNCTION public.notify_on_scheduling();
|
||||
|
||||
|
||||
--
|
||||
-- Name: agenda_eventos trg_notify_on_session_status; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_notify_on_session_status AFTER UPDATE OF status ON public.agenda_eventos FOR EACH ROW EXECUTE FUNCTION public.notify_on_session_status();
|
||||
|
||||
|
||||
--
|
||||
-- Name: tenant_members trg_patient_cannot_own_tenant; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_patient_cannot_own_tenant BEFORE INSERT OR UPDATE ON public.tenant_members FOR EACH ROW EXECUTE FUNCTION public.guard_patient_cannot_own_tenant();
|
||||
|
||||
|
||||
--
|
||||
-- Name: patient_groups trg_patient_groups_set_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_patient_groups_set_updated_at BEFORE UPDATE ON public.patient_groups FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: patient_intake_requests trg_patient_intake_requests_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_patient_intake_requests_updated_at BEFORE UPDATE ON public.patient_intake_requests FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: patient_tags trg_patient_tags_set_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_patient_tags_set_updated_at BEFORE UPDATE ON public.patient_tags FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: patients trg_patients_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_patients_updated_at BEFORE UPDATE ON public.patients FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: patients trg_patients_validate_members; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_patients_validate_members BEFORE INSERT OR UPDATE OF tenant_id, responsible_member_id, patient_scope, therapist_member_id ON public.patients FOR EACH ROW EXECUTE FUNCTION public.patients_validate_member_consistency();
|
||||
|
||||
|
||||
--
|
||||
-- Name: payment_settings trg_payment_settings_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_payment_settings_updated_at BEFORE UPDATE ON public.payment_settings FOR EACH ROW EXECUTE FUNCTION public.update_payment_settings_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: patient_groups trg_prevent_promoting_to_system; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_prevent_promoting_to_system BEFORE UPDATE ON public.patient_groups FOR EACH ROW EXECUTE FUNCTION public.prevent_promoting_to_system();
|
||||
|
||||
|
||||
--
|
||||
-- Name: patient_groups trg_prevent_system_group_changes; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_prevent_system_group_changes BEFORE DELETE OR UPDATE ON public.patient_groups FOR EACH ROW EXECUTE FUNCTION public.prevent_system_group_changes();
|
||||
|
||||
|
||||
--
|
||||
-- Name: professional_pricing trg_professional_pricing_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_professional_pricing_updated_at BEFORE UPDATE ON public.professional_pricing FOR EACH ROW EXECUTE FUNCTION public.update_professional_pricing_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: profiles trg_profiles_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_profiles_updated_at BEFORE UPDATE ON public.profiles FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: services trg_services_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_services_updated_at BEFORE UPDATE ON public.services FOR EACH ROW EXECUTE FUNCTION public.set_services_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: subscription_intents trg_subscription_intents_view_insert; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_subscription_intents_view_insert INSTEAD OF INSERT ON public.subscription_intents FOR EACH ROW EXECUTE FUNCTION public.subscription_intents_view_insert();
|
||||
|
||||
|
||||
--
|
||||
-- Name: subscriptions trg_subscriptions_validate_scope; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_subscriptions_validate_scope BEFORE INSERT OR UPDATE ON public.subscriptions FOR EACH ROW EXECUTE FUNCTION public.subscriptions_validate_scope();
|
||||
|
||||
|
||||
--
|
||||
-- Name: tenant_features trg_tenant_features_guard_with_plan; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_tenant_features_guard_with_plan BEFORE INSERT OR UPDATE ON public.tenant_features FOR EACH ROW EXECUTE FUNCTION public.tenant_features_guard_with_plan();
|
||||
|
||||
|
||||
--
|
||||
-- Name: tenant_features trg_tenant_features_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_tenant_features_updated_at BEFORE UPDATE ON public.tenant_features FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: tenants trg_tenant_kind_immutable; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_tenant_kind_immutable BEFORE UPDATE OF kind ON public.tenants FOR EACH ROW EXECUTE FUNCTION public.guard_tenant_kind_immutable();
|
||||
|
||||
|
||||
--
|
||||
-- Name: therapist_payouts trg_therapist_payouts_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_therapist_payouts_updated_at BEFORE UPDATE ON public.therapist_payouts FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_settings trg_user_settings_updated_at; Type: TRIGGER; Schema: public; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER trg_user_settings_updated_at BEFORE UPDATE ON public.user_settings FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
|
||||
--
|
||||
-- Name: subscription tr_check_filters; Type: TRIGGER; Schema: realtime; Owner: supabase_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER tr_check_filters BEFORE INSERT OR UPDATE ON realtime.subscription FOR EACH ROW EXECUTE FUNCTION realtime.subscription_check_filters();
|
||||
|
||||
|
||||
--
|
||||
-- Name: buckets enforce_bucket_name_length_trigger; Type: TRIGGER; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER enforce_bucket_name_length_trigger BEFORE INSERT OR UPDATE OF name ON storage.buckets FOR EACH ROW EXECUTE FUNCTION storage.enforce_bucket_name_length();
|
||||
|
||||
|
||||
--
|
||||
-- Name: buckets protect_buckets_delete; Type: TRIGGER; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER protect_buckets_delete BEFORE DELETE ON storage.buckets FOR EACH STATEMENT EXECUTE FUNCTION storage.protect_delete();
|
||||
|
||||
|
||||
--
|
||||
-- Name: objects protect_objects_delete; Type: TRIGGER; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER protect_objects_delete BEFORE DELETE ON storage.objects FOR EACH STATEMENT EXECUTE FUNCTION storage.protect_delete();
|
||||
|
||||
|
||||
--
|
||||
-- Name: objects update_objects_updated_at; Type: TRIGGER; Schema: storage; Owner: supabase_storage_admin
|
||||
--
|
||||
|
||||
CREATE TRIGGER update_objects_updated_at BEFORE UPDATE ON storage.objects FOR EACH ROW EXECUTE FUNCTION storage.update_updated_at_column();
|
||||
2313
database-novo/schema/09_policies/policies.sql
Normal file
2313
database-novo/schema/09_policies/policies.sql
Normal file
File diff suppressed because it is too large
Load Diff
6534
database-novo/schema/10_grants/grants.sql
Normal file
6534
database-novo/schema/10_grants/grants.sql
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user