05c6746e33
- 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>
168 lines
4.8 KiB
PL/PgSQL
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;
|