Files
agenciapsilmno/database-novo/migrations/20260612000006_f2_provision_clone.sql
T
Leonardo 05c6746e33 schema-per-tenant: F0 categorizacao + F1 template/helpers + F2 provisionamento
- docs/F0_categorizacao.md: varredura completa (137 tabelas -> 84 tenant + 53
  global, 66 funcoes, FKs, policies, edge functions) + decisoes Q1-Q4
- F1 (migrations 01-05): tenants.slug, helpers de schema, _tenant_template
  (84 tabelas sem tenant_id, singletons, views __SCHEMA__/__TENANT_ID__),
  clone_tenant_template/drop_tenant_schema, channel_routing, tenant_schemas
- F2 (migration 06): provision_account_tenant/create_clinic_tenant/
  ensure_personal_tenant_for_user clonam schema na mesma transacao
- db.cjs: psqlFile agora usa ON_ERROR_STOP=1 (falha de migration nao passa
  mais como sucesso silencioso)
- blueprint original em novo-rumo.txt; wiki Obsidian atualizada

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:58:46 -03:00

168 lines
4.8 KiB
PL/PgSQL

-- =============================================================================
-- F2 — Schema-per-tenant: provisionamento cria o schema físico
--
-- Os 3 pontos de criação de tenant passam a chamar clone_tenant_template()
-- logo após inserir em tenants/tenant_members. Tudo na mesma transação:
-- se o clone falhar, o tenant não nasce (atomicidade).
--
-- Pontos cobertos (F0 §levantamento — não há outros INSERT INTO tenants):
-- * provision_account_tenant — wizard de cadastro (therapist/clinic_*)
-- * create_clinic_tenant — criação avulsa de clínica
-- * ensure_personal_tenant_for_user — tenant pessoal (kind='saas'),
-- chamado também pelo trigger de signup (handle_new_user_create_personal_tenant)
--
-- Decisão Q2: TODO tenant ganha schema, inclusive therapist e pessoal.
-- Clones nascem sem triggers de negócio (F6) e fora do PostgREST (F5).
-- =============================================================================
BEGIN;
CREATE OR REPLACE FUNCTION public.create_clinic_tenant(p_name text)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
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());
-- F2: schema físico do tenant (rollback junto se algo falhar)
perform public.clone_tenant_template(v_tenant);
return v_tenant;
end;
$function$;
CREATE OR REPLACE FUNCTION public.ensure_personal_tenant_for_user(p_user_id uuid)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
declare
v_uid uuid;
v_existing uuid;
v_tenant uuid;
v_email text;
v_name text;
begin
v_uid := p_user_id;
if v_uid is null then
raise exception 'Missing user id';
end if;
-- só considera tenant pessoal (kind='saas')
select tm.tenant_id
into v_existing
from public.tenant_members tm
join public.tenants t on t.id = tm.tenant_id
where tm.user_id = v_uid
and tm.status = 'active'
and t.kind = 'saas'
order by tm.created_at desc
limit 1;
if v_existing is not null then
return v_existing;
end if;
select email into v_email
from auth.users
where id = v_uid;
v_name := coalesce(split_part(v_email, '@', 1), 'Conta');
insert into public.tenants (name, kind, created_at)
values (v_name || ' (Pessoal)', 'saas', 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());
-- F2: schema físico do tenant (rollback junto se algo falhar)
perform public.clone_tenant_template(v_tenant);
return v_tenant;
end;
$function$;
CREATE OR REPLACE FUNCTION public.provision_account_tenant(p_user_id uuid, p_kind text, p_name text DEFAULT NULL::text)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
DECLARE
v_tenant_id uuid;
v_account_type text;
v_name text;
BEGIN
IF p_kind NOT IN ('therapist', 'clinic_coworking', 'clinic_reception', 'clinic_full') THEN
RAISE EXCEPTION 'kind inválido: "%". Use: therapist, clinic_coworking, clinic_reception, clinic_full.', p_kind
USING ERRCODE = 'P0001';
END IF;
v_account_type := CASE WHEN p_kind = 'therapist' THEN 'therapist' ELSE 'clinic' END;
IF EXISTS (
SELECT 1
FROM public.tenant_members tm
JOIN public.tenants t ON t.id = tm.tenant_id
WHERE tm.user_id = p_user_id
AND tm.role = 'tenant_admin'
AND tm.status = 'active'
AND t.kind = p_kind
) THEN
RAISE EXCEPTION 'Usuário já possui um tenant do tipo "%".', p_kind
USING ERRCODE = 'P0001';
END IF;
v_name := COALESCE(
NULLIF(TRIM(p_name), ''),
(
SELECT COALESCE(NULLIF(TRIM(pr.full_name), ''), SPLIT_PART(au.email, '@', 1))
FROM public.profiles pr
JOIN auth.users au ON au.id = pr.id
WHERE pr.id = p_user_id
),
'Conta'
);
INSERT INTO public.tenants (name, kind, created_at)
VALUES (v_name, p_kind, now())
RETURNING id INTO v_tenant_id;
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
VALUES (v_tenant_id, p_user_id, 'tenant_admin', 'active', now());
UPDATE public.profiles
SET account_type = v_account_type
WHERE id = p_user_id;
PERFORM public.seed_determined_commitments(v_tenant_id);
-- F2: schema físico do tenant (rollback junto se algo falhar)
PERFORM public.clone_tenant_template(v_tenant_id);
RETURN v_tenant_id;
END;
$function$;
COMMIT;