-- ========================================================================== -- Agencia PSI — Migracao: Emails polimorficos com tipo + principal -- ========================================================================== -- Criado por: Leonardo Nohama -- Data: 2026-04-21 · Sao Carlos/SP — Brasil -- -- Mesmo padrao dos telefones (migration 20260421000008): -- - contact_email_types → tipos configuraveis (Principal, Comercial, Pessoal, ...) -- - contact_emails → emails polimorficos (entity_type + entity_id) -- -- Triggers mantem patients.email_principal/email_alternativo e medicos.email -- sincronizados pra nao quebrar codigo legado. -- ========================================================================== -- --------------------------------------------------------------------------- -- Tabela: contact_email_types -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.contact_email_types ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES public.tenants(id) ON DELETE CASCADE, name TEXT NOT NULL CHECK (length(name) > 0 AND length(name) <= 40), slug TEXT NOT NULL CHECK (slug ~ '^[a-z0-9_-]{1,40}$'), icon TEXT, is_system BOOLEAN NOT NULL DEFAULT false, position INT NOT NULL DEFAULT 100, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX IF NOT EXISTS uq_contact_email_types_tenant_slug ON public.contact_email_types (tenant_id, slug) WHERE tenant_id IS NOT NULL; CREATE UNIQUE INDEX IF NOT EXISTS uq_contact_email_types_system_slug ON public.contact_email_types (slug) WHERE tenant_id IS NULL; CREATE INDEX IF NOT EXISTS idx_contact_email_types_tenant ON public.contact_email_types (tenant_id, position); DROP TRIGGER IF EXISTS trg_contact_email_types_updated_at ON public.contact_email_types; CREATE TRIGGER trg_contact_email_types_updated_at BEFORE UPDATE ON public.contact_email_types FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); COMMENT ON TABLE public.contact_email_types IS 'Tipos de email (Principal, Comercial, Pessoal, ...). System (tenant_id NULL) + custom.'; -- Seed INSERT INTO public.contact_email_types (tenant_id, name, slug, icon, is_system, position) VALUES (NULL, 'Principal', 'principal', 'pi pi-envelope', true, 10), (NULL, 'Pessoal', 'pessoal', 'pi pi-user', true, 20), (NULL, 'Comercial', 'comercial', 'pi pi-building', true, 30), (NULL, 'Faturamento', 'faturamento', 'pi pi-dollar', true, 40), (NULL, 'Alternativo', 'alternativo', 'pi pi-reply', true, 50) ON CONFLICT DO NOTHING; -- --------------------------------------------------------------------------- -- Tabela: contact_emails (polimorfica) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.contact_emails ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, entity_type TEXT NOT NULL CHECK (entity_type IN ('patient', 'medico')), entity_id UUID NOT NULL, contact_email_type_id UUID NOT NULL REFERENCES public.contact_email_types(id) ON DELETE RESTRICT, email TEXT NOT NULL CHECK (email ~* '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$'), is_primary BOOLEAN NOT NULL DEFAULT false, notes TEXT, position INT NOT NULL DEFAULT 100, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_contact_emails_entity ON public.contact_emails (tenant_id, entity_type, entity_id, position); CREATE INDEX IF NOT EXISTS idx_contact_emails_email ON public.contact_emails (tenant_id, email); CREATE UNIQUE INDEX IF NOT EXISTS uq_contact_emails_primary ON public.contact_emails (entity_type, entity_id) WHERE is_primary = true; DROP TRIGGER IF EXISTS trg_contact_emails_updated_at ON public.contact_emails; CREATE TRIGGER trg_contact_emails_updated_at BEFORE UPDATE ON public.contact_emails FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); COMMENT ON TABLE public.contact_emails IS 'Emails polimorficos (patients, medicos, ...). Max 1 primary por entidade. Triggers sincronizam campos legados.'; -- --------------------------------------------------------------------------- -- Trigger: sincroniza campos legados apos mudanca -- --------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION public.sync_legacy_email_fields() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_entity_type TEXT; v_entity_id UUID; v_primary TEXT; v_secondary TEXT; BEGIN IF TG_OP = 'DELETE' THEN v_entity_type := OLD.entity_type; v_entity_id := OLD.entity_id; ELSE v_entity_type := NEW.entity_type; v_entity_id := NEW.entity_id; END IF; SELECT email INTO v_primary FROM public.contact_emails WHERE entity_type = v_entity_type AND entity_id = v_entity_id ORDER BY is_primary DESC, position ASC, created_at ASC LIMIT 1; SELECT email INTO v_secondary FROM public.contact_emails WHERE entity_type = v_entity_type AND entity_id = v_entity_id AND is_primary = false ORDER BY position ASC, created_at ASC OFFSET 0 LIMIT 1; IF v_entity_type = 'patient' THEN UPDATE public.patients SET email_principal = v_primary, email_alternativo = v_secondary WHERE id = v_entity_id; ELSIF v_entity_type = 'medico' THEN UPDATE public.medicos SET email = v_primary WHERE id = v_entity_id; END IF; IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF; END; $$; DROP TRIGGER IF EXISTS trg_contact_emails_sync_legacy ON public.contact_emails; CREATE TRIGGER trg_contact_emails_sync_legacy AFTER INSERT OR UPDATE OR DELETE ON public.contact_emails FOR EACH ROW EXECUTE FUNCTION public.sync_legacy_email_fields(); -- --------------------------------------------------------------------------- -- Backfill: migra emails existentes -- --------------------------------------------------------------------------- DO $$ DECLARE v_principal_id UUID; v_alternativo_id UUID; BEGIN SELECT id INTO v_principal_id FROM public.contact_email_types WHERE slug = 'principal' AND tenant_id IS NULL LIMIT 1; SELECT id INTO v_alternativo_id FROM public.contact_email_types WHERE slug = 'alternativo' AND tenant_id IS NULL LIMIT 1; -- Patients.email_principal → Principal primary INSERT INTO public.contact_emails (tenant_id, entity_type, entity_id, contact_email_type_id, email, is_primary, position) SELECT p.tenant_id, 'patient', p.id, v_principal_id, lower(trim(p.email_principal)), true, 10 FROM public.patients p WHERE p.email_principal IS NOT NULL AND trim(p.email_principal) ~* '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$' AND NOT EXISTS ( SELECT 1 FROM public.contact_emails ce WHERE ce.entity_type = 'patient' AND ce.entity_id = p.id ) ON CONFLICT DO NOTHING; -- Patients.email_alternativo → Alternativo INSERT INTO public.contact_emails (tenant_id, entity_type, entity_id, contact_email_type_id, email, is_primary, position) SELECT p.tenant_id, 'patient', p.id, v_alternativo_id, lower(trim(p.email_alternativo)), false, 20 FROM public.patients p WHERE p.email_alternativo IS NOT NULL AND trim(p.email_alternativo) ~* '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$' AND NOT EXISTS ( SELECT 1 FROM public.contact_emails ce WHERE ce.entity_type = 'patient' AND ce.entity_id = p.id AND ce.email = lower(trim(p.email_alternativo)) ) ON CONFLICT DO NOTHING; -- Medicos.email → Principal primary INSERT INTO public.contact_emails (tenant_id, entity_type, entity_id, contact_email_type_id, email, is_primary, position) SELECT m.tenant_id, 'medico', m.id, v_principal_id, lower(trim(m.email)), true, 10 FROM public.medicos m WHERE m.email IS NOT NULL AND trim(m.email) ~* '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$' AND NOT EXISTS ( SELECT 1 FROM public.contact_emails ce WHERE ce.entity_type = 'medico' AND ce.entity_id = m.id ) ON CONFLICT DO NOTHING; END $$; -- --------------------------------------------------------------------------- -- RLS -- --------------------------------------------------------------------------- ALTER TABLE public.contact_email_types ENABLE ROW LEVEL SECURITY; ALTER TABLE public.contact_emails ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "contact_email_types: select" ON public.contact_email_types; CREATE POLICY "contact_email_types: select" ON public.contact_email_types FOR SELECT TO authenticated USING ( tenant_id IS NULL OR public.is_saas_admin() OR EXISTS ( SELECT 1 FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.tenant_id = contact_email_types.tenant_id AND tm.status = 'active' ) ); DROP POLICY IF EXISTS "contact_email_types: manage custom" ON public.contact_email_types; CREATE POLICY "contact_email_types: manage custom" ON public.contact_email_types FOR ALL TO authenticated USING ( is_system = false AND tenant_id IS NOT NULL AND ( public.is_saas_admin() OR EXISTS ( SELECT 1 FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.tenant_id = contact_email_types.tenant_id AND tm.status = 'active' ) ) ) WITH CHECK ( is_system = false AND tenant_id IS NOT NULL AND ( public.is_saas_admin() OR EXISTS ( SELECT 1 FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.tenant_id = contact_email_types.tenant_id AND tm.status = 'active' ) ) ); DROP POLICY IF EXISTS "contact_emails: all tenant" ON public.contact_emails; CREATE POLICY "contact_emails: all tenant" ON public.contact_emails FOR ALL TO authenticated USING ( public.is_saas_admin() OR EXISTS ( SELECT 1 FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.tenant_id = contact_emails.tenant_id AND tm.status = 'active' ) ) WITH CHECK ( public.is_saas_admin() OR EXISTS ( SELECT 1 FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.tenant_id = contact_emails.tenant_id AND tm.status = 'active' ) ); -- ========================================================================== -- FIM DA MIGRACAO -- ==========================================================================