Agenda, Agendador, Configurações
This commit is contained in:
414
DBS/2026-03-11/Nova-Dev-Doc/supervisor_fase1.sql
Normal file
414
DBS/2026-03-11/Nova-Dev-Doc/supervisor_fase1.sql
Normal file
@@ -0,0 +1,414 @@
|
||||
-- ============================================================
|
||||
-- SUPERVISOR — Fase 1
|
||||
-- Aplicar no Supabase SQL Editor (em ordem)
|
||||
-- ============================================================
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 1. tenants.kind → adiciona 'supervisor'
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
ALTER TABLE public.tenants
|
||||
DROP CONSTRAINT IF EXISTS tenants_kind_check;
|
||||
|
||||
ALTER TABLE public.tenants
|
||||
ADD CONSTRAINT tenants_kind_check
|
||||
CHECK (kind = ANY (ARRAY[
|
||||
'therapist',
|
||||
'clinic_coworking',
|
||||
'clinic_reception',
|
||||
'clinic_full',
|
||||
'clinic',
|
||||
'saas',
|
||||
'supervisor' -- ← novo
|
||||
]));
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 2. plans.target → adiciona 'supervisor'
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
ALTER TABLE public.plans
|
||||
DROP CONSTRAINT IF EXISTS plans_target_check;
|
||||
|
||||
ALTER TABLE public.plans
|
||||
ADD CONSTRAINT plans_target_check
|
||||
CHECK (target = ANY (ARRAY[
|
||||
'patient',
|
||||
'therapist',
|
||||
'clinic',
|
||||
'supervisor' -- ← novo
|
||||
]));
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 3. plans.max_supervisees — limite de supervisionados
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
ALTER TABLE public.plans
|
||||
ADD COLUMN IF NOT EXISTS max_supervisees integer DEFAULT NULL;
|
||||
|
||||
COMMENT ON COLUMN public.plans.max_supervisees IS
|
||||
'Limite de terapeutas que podem ser supervisionados. Apenas para planos target=supervisor. NULL = sem limite.';
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 4. Planos supervisor_free e supervisor_pro
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
INSERT INTO public.plans (key, name, description, target, is_active, max_supervisees)
|
||||
VALUES
|
||||
(
|
||||
'supervisor_free',
|
||||
'Supervisor Free',
|
||||
'Plano gratuito de supervisão. Até 3 terapeutas supervisionados.',
|
||||
'supervisor',
|
||||
true,
|
||||
3
|
||||
),
|
||||
(
|
||||
'supervisor_pro',
|
||||
'Supervisor PRO',
|
||||
'Plano profissional de supervisão. Até 20 terapeutas supervisionados.',
|
||||
'supervisor',
|
||||
true,
|
||||
20
|
||||
)
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET
|
||||
name = EXCLUDED.name,
|
||||
description = EXCLUDED.description,
|
||||
target = EXCLUDED.target,
|
||||
is_active = EXCLUDED.is_active,
|
||||
max_supervisees = EXCLUDED.max_supervisees;
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 5. Features de supervisor
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
INSERT INTO public.features (key, name, descricao)
|
||||
VALUES
|
||||
(
|
||||
'supervisor.access',
|
||||
'Acesso à Supervisão',
|
||||
'Acesso básico ao espaço de supervisão (sala, lista de supervisionados).'
|
||||
),
|
||||
(
|
||||
'supervisor.invite',
|
||||
'Convidar Supervisionados',
|
||||
'Permite convidar terapeutas para participar da sala de supervisão.'
|
||||
),
|
||||
(
|
||||
'supervisor.sessions',
|
||||
'Sessões de Supervisão',
|
||||
'Agendamento e registro de sessões de supervisão.'
|
||||
),
|
||||
(
|
||||
'supervisor.reports',
|
||||
'Relatórios de Supervisão',
|
||||
'Relatórios avançados de progresso e evolução dos supervisionados.'
|
||||
)
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET
|
||||
name = EXCLUDED.name,
|
||||
descricao = EXCLUDED.descricao;
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 6. plan_features — vincula features aos planos supervisor
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
DO $$
|
||||
DECLARE
|
||||
v_free_id uuid;
|
||||
v_pro_id uuid;
|
||||
v_f_access uuid;
|
||||
v_f_invite uuid;
|
||||
v_f_sessions uuid;
|
||||
v_f_reports uuid;
|
||||
BEGIN
|
||||
SELECT id INTO v_free_id FROM public.plans WHERE key = 'supervisor_free';
|
||||
SELECT id INTO v_pro_id FROM public.plans WHERE key = 'supervisor_pro';
|
||||
|
||||
SELECT id INTO v_f_access FROM public.features WHERE key = 'supervisor.access';
|
||||
SELECT id INTO v_f_invite FROM public.features WHERE key = 'supervisor.invite';
|
||||
SELECT id INTO v_f_sessions FROM public.features WHERE key = 'supervisor.sessions';
|
||||
SELECT id INTO v_f_reports FROM public.features WHERE key = 'supervisor.reports';
|
||||
|
||||
-- supervisor_free: access + invite (limitado por max_supervisees=3)
|
||||
INSERT INTO public.plan_features (plan_id, feature_id)
|
||||
VALUES
|
||||
(v_free_id, v_f_access),
|
||||
(v_free_id, v_f_invite)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- supervisor_pro: tudo
|
||||
INSERT INTO public.plan_features (plan_id, feature_id)
|
||||
VALUES
|
||||
(v_pro_id, v_f_access),
|
||||
(v_pro_id, v_f_invite),
|
||||
(v_pro_id, v_f_sessions),
|
||||
(v_pro_id, v_f_reports)
|
||||
ON CONFLICT DO NOTHING;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 7. activate_subscription_from_intent — suporte a supervisor
|
||||
-- Supervisor = pessoal (user_id), sem tenant_id (igual therapist)
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE FUNCTION public.activate_subscription_from_intent(p_intent_id uuid)
|
||||
RETURNS public.subscriptions
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
AS $$
|
||||
declare
|
||||
v_intent record;
|
||||
v_sub public.subscriptions;
|
||||
v_days int;
|
||||
v_user_id uuid;
|
||||
v_plan_id uuid;
|
||||
v_target text;
|
||||
begin
|
||||
-- lê pela VIEW unificada
|
||||
select * into v_intent
|
||||
from public.subscription_intents
|
||||
where id = p_intent_id;
|
||||
|
||||
if not found then
|
||||
raise exception 'Intent não encontrado: %', p_intent_id;
|
||||
end if;
|
||||
|
||||
if v_intent.status <> 'paid' then
|
||||
raise exception 'Intent precisa estar paid para ativar assinatura';
|
||||
end if;
|
||||
|
||||
-- resolve target e plan_id via plans.key
|
||||
select p.id, p.target
|
||||
into v_plan_id, v_target
|
||||
from public.plans p
|
||||
where p.key = v_intent.plan_key
|
||||
limit 1;
|
||||
|
||||
if v_plan_id is null then
|
||||
raise exception 'Plano não encontrado em plans.key = %', v_intent.plan_key;
|
||||
end if;
|
||||
|
||||
v_target := lower(coalesce(v_target, ''));
|
||||
|
||||
-- ✅ supervisor adicionado
|
||||
if v_target not in ('clinic', 'therapist', 'supervisor') then
|
||||
raise exception 'Target inválido em plans.target: %', v_target;
|
||||
end if;
|
||||
|
||||
-- regra por target
|
||||
if v_target = 'clinic' then
|
||||
if v_intent.tenant_id is null then
|
||||
raise exception 'Intent sem tenant_id';
|
||||
end if;
|
||||
else
|
||||
-- therapist ou supervisor: vinculado ao user
|
||||
v_user_id := v_intent.user_id;
|
||||
if v_user_id is null then
|
||||
v_user_id := v_intent.created_by_user_id;
|
||||
end if;
|
||||
end if;
|
||||
|
||||
if v_target in ('therapist', 'supervisor') and v_user_id is null then
|
||||
raise exception 'Não foi possível determinar user_id para assinatura %.', v_target;
|
||||
end if;
|
||||
|
||||
-- cancela assinatura ativa anterior
|
||||
if v_target = 'clinic' then
|
||||
update public.subscriptions
|
||||
set status = 'cancelled',
|
||||
cancelled_at = now()
|
||||
where tenant_id = v_intent.tenant_id
|
||||
and plan_id = v_plan_id
|
||||
and status = 'active';
|
||||
else
|
||||
-- therapist ou supervisor
|
||||
update public.subscriptions
|
||||
set status = 'cancelled',
|
||||
cancelled_at = now()
|
||||
where user_id = v_user_id
|
||||
and plan_id = v_plan_id
|
||||
and status = 'active'
|
||||
and tenant_id is null;
|
||||
end if;
|
||||
|
||||
-- duração do plano (30 dias para mensal)
|
||||
v_days := case
|
||||
when lower(coalesce(v_intent.interval, 'month')) = 'year' then 365
|
||||
else 30
|
||||
end;
|
||||
|
||||
-- cria nova assinatura
|
||||
insert into public.subscriptions (
|
||||
user_id,
|
||||
plan_id,
|
||||
status,
|
||||
started_at,
|
||||
expires_at,
|
||||
cancelled_at,
|
||||
activated_at,
|
||||
tenant_id,
|
||||
plan_key,
|
||||
interval,
|
||||
source,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
values (
|
||||
case when v_target = 'clinic' then null else v_user_id end,
|
||||
v_plan_id,
|
||||
'active',
|
||||
now(),
|
||||
now() + make_interval(days => v_days),
|
||||
null,
|
||||
now(),
|
||||
case when v_target = 'clinic' then v_intent.tenant_id else null end,
|
||||
v_intent.plan_key,
|
||||
v_intent.interval,
|
||||
'manual',
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
returning * into v_sub;
|
||||
|
||||
-- grava vínculo intent → subscription
|
||||
if v_target = 'clinic' then
|
||||
update public.subscription_intents_tenant
|
||||
set subscription_id = v_sub.id
|
||||
where id = p_intent_id;
|
||||
else
|
||||
update public.subscription_intents_personal
|
||||
set subscription_id = v_sub.id
|
||||
where id = p_intent_id;
|
||||
end if;
|
||||
|
||||
return v_sub;
|
||||
end;
|
||||
$$;
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 8. subscriptions_validate_scope — suporte a supervisor
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE FUNCTION public.subscriptions_validate_scope()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_target text;
|
||||
BEGIN
|
||||
SELECT lower(p.target) INTO v_target
|
||||
FROM public.plans p
|
||||
WHERE p.id = NEW.plan_id;
|
||||
|
||||
IF v_target IS NULL THEN
|
||||
RAISE EXCEPTION 'Plano inválido (target nulo).';
|
||||
END IF;
|
||||
|
||||
IF v_target = 'clinic' THEN
|
||||
IF NEW.tenant_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura clinic exige tenant_id.';
|
||||
END IF;
|
||||
IF NEW.user_id IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura clinic não pode ter user_id (XOR).';
|
||||
END IF;
|
||||
|
||||
ELSIF v_target IN ('therapist', 'supervisor') THEN
|
||||
-- supervisor é pessoal como therapist
|
||||
IF NEW.tenant_id IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura % não deve ter tenant_id.', v_target;
|
||||
END IF;
|
||||
IF NEW.user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura % exige user_id.', v_target;
|
||||
END IF;
|
||||
|
||||
ELSIF v_target = 'patient' THEN
|
||||
IF NEW.tenant_id IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura patient não deve ter tenant_id.';
|
||||
END IF;
|
||||
IF NEW.user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Assinatura patient exige user_id.';
|
||||
END IF;
|
||||
|
||||
ELSE
|
||||
RAISE EXCEPTION 'Target de plano inválido: %', v_target;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- 9. subscription_intents_view_insert — suporte a supervisor
|
||||
-- supervisor é roteado como therapist (tabela personal)
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE FUNCTION public.subscription_intents_view_insert()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
AS $$
|
||||
declare
|
||||
v_target text;
|
||||
v_plan_id uuid;
|
||||
begin
|
||||
select p.id, p.target into v_plan_id, v_target
|
||||
from public.plans p
|
||||
where p.key = new.plan_key;
|
||||
|
||||
if v_plan_id is null then
|
||||
raise exception 'Plano inválido: plan_key=%', new.plan_key;
|
||||
end if;
|
||||
|
||||
if lower(v_target) = 'clinic' then
|
||||
if new.tenant_id is null then
|
||||
raise exception 'Intenção clinic exige tenant_id.';
|
||||
end if;
|
||||
|
||||
insert into public.subscription_intents_tenant (
|
||||
id, tenant_id, created_by_user_id, email,
|
||||
plan_id, plan_key, interval, amount_cents, currency,
|
||||
status, source, notes, created_at, paid_at
|
||||
) values (
|
||||
coalesce(new.id, gen_random_uuid()),
|
||||
new.tenant_id, new.created_by_user_id, new.email,
|
||||
v_plan_id, new.plan_key, coalesce(new.interval,'month'),
|
||||
new.amount_cents, coalesce(new.currency,'BRL'),
|
||||
coalesce(new.status,'pending'), coalesce(new.source,'manual'),
|
||||
new.notes, coalesce(new.created_at, now()), new.paid_at
|
||||
);
|
||||
|
||||
new.plan_target := 'clinic';
|
||||
return new;
|
||||
end if;
|
||||
|
||||
-- therapist ou supervisor → tabela personal
|
||||
if lower(v_target) in ('therapist', 'supervisor') then
|
||||
insert into public.subscription_intents_personal (
|
||||
id, user_id, created_by_user_id, email,
|
||||
plan_id, plan_key, interval, amount_cents, currency,
|
||||
status, source, notes, created_at, paid_at
|
||||
) values (
|
||||
coalesce(new.id, gen_random_uuid()),
|
||||
new.user_id, new.created_by_user_id, new.email,
|
||||
v_plan_id, new.plan_key, coalesce(new.interval,'month'),
|
||||
new.amount_cents, coalesce(new.currency,'BRL'),
|
||||
coalesce(new.status,'pending'), coalesce(new.source,'manual'),
|
||||
new.notes, coalesce(new.created_at, now()), new.paid_at
|
||||
);
|
||||
|
||||
new.plan_target := lower(v_target); -- 'therapist' ou 'supervisor'
|
||||
return new;
|
||||
end if;
|
||||
|
||||
raise exception 'Target de plano não suportado: %', v_target;
|
||||
end;
|
||||
$$;
|
||||
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- FIM — verificação rápida
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
SELECT key, name, target, max_supervisees
|
||||
FROM public.plans
|
||||
WHERE target = 'supervisor'
|
||||
ORDER BY key;
|
||||
Reference in New Issue
Block a user