415 lines
14 KiB
PL/PgSQL
415 lines
14 KiB
PL/PgSQL
-- ============================================================
|
|
-- 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;
|