-- ========================================================================== -- Agencia PSI — Migracao: Opt-out de conversas (CRM Grupo 5.2, LGPD) -- ========================================================================== -- Criado por: Leonardo Nohama -- Data: 2026-04-21 · Sao Carlos/SP — Brasil -- -- Quando paciente envia keyword de opt-out (PARAR, SAIR, CANCELAR, STOP...), -- bloqueia envios automaticos (auto-reply + futuros lembretes). -- -- LGPD: direito de oposicao (Art. 18, §2). Pedido de interrupcao deve ser -- respeitado. Mensagens manuais do terapeuta nao sao bloqueadas — relacao -- terapeutica existe. -- -- Phone e normalizado (apenas digitos) pra matching consistente. -- ========================================================================== CREATE TABLE IF NOT EXISTS public.conversation_optouts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, phone TEXT NOT NULL CHECK (phone ~ '^\d{6,15}$'), patient_id UUID REFERENCES public.patients(id) ON DELETE SET NULL, -- 'keyword' = detectado automaticamente por palavra-chave -- 'manual' = adicionado manualmente pelo terapeuta/admin source TEXT NOT NULL DEFAULT 'keyword' CHECK (source IN ('keyword', 'manual')), keyword_matched TEXT, -- palavra/frase que disparou (quando source='keyword') original_message TEXT, -- texto completo da msg original (truncado) notes TEXT, -- observacao do terapeuta (quando manual) blocked_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, -- quem adicionou manual opted_out_at TIMESTAMPTZ NOT NULL DEFAULT now(), opted_back_in_at TIMESTAMPTZ, -- quando usuario restaurou (opt-in) created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Unique: um registro ativo por tenant+phone. Permite historico se fizer opt-in e depois opt-out de novo. -- Active = opted_back_in_at IS NULL CREATE UNIQUE INDEX IF NOT EXISTS uq_conv_optouts_active ON public.conversation_optouts (tenant_id, phone) WHERE opted_back_in_at IS NULL; CREATE INDEX IF NOT EXISTS idx_conv_optouts_tenant_phone ON public.conversation_optouts (tenant_id, phone); CREATE INDEX IF NOT EXISTS idx_conv_optouts_patient ON public.conversation_optouts (patient_id) WHERE patient_id IS NOT NULL; DROP TRIGGER IF EXISTS trg_conv_optouts_updated_at ON public.conversation_optouts; CREATE TRIGGER trg_conv_optouts_updated_at BEFORE UPDATE ON public.conversation_optouts FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); COMMENT ON TABLE public.conversation_optouts IS 'Numeros que pediram pra nao receber mensagens automaticas. LGPD Art. 18 Sec.2.'; -- --------------------------------------------------------------------------- -- Keywords de opt-out — lista do tenant (reutilizavel) -- Cada tenant pode customizar suas palavras-chave. Default aplicado via seed. -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.conversation_optout_keywords ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES public.tenants(id) ON DELETE CASCADE, -- NULL = system (todos) keyword TEXT NOT NULL CHECK (length(keyword) > 0 AND length(keyword) <= 100), enabled BOOLEAN NOT NULL DEFAULT true, is_system BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_conv_optout_kw_tenant ON public.conversation_optout_keywords (tenant_id) WHERE enabled = true; -- Seed keywords padrao (system, tenant_id NULL, todos veem) INSERT INTO public.conversation_optout_keywords (tenant_id, keyword, is_system, enabled) VALUES (NULL, 'parar', true, true), (NULL, 'sair', true, true), (NULL, 'cancelar', true, true), (NULL, 'stop', true, true), (NULL, 'descadastrar', true, true), (NULL, 'remover', true, true), (NULL, 'nao quero mais', true, true), (NULL, 'não quero mais', true, true), (NULL, 'desinscrever', true, true), (NULL, 'unsubscribe', true, true) ON CONFLICT DO NOTHING; COMMENT ON TABLE public.conversation_optout_keywords IS 'Palavras-chave que disparam opt-out quando paciente envia. Sistema (tenant_id NULL) + custom do tenant.'; -- --------------------------------------------------------------------------- -- RLS: optouts -- --------------------------------------------------------------------------- ALTER TABLE public.conversation_optouts ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "optouts: select" ON public.conversation_optouts; CREATE POLICY "optouts: select" ON public.conversation_optouts FOR SELECT 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 = conversation_optouts.tenant_id AND tm.status = 'active' ) ); DROP POLICY IF EXISTS "optouts: insert" ON public.conversation_optouts; CREATE POLICY "optouts: insert" ON public.conversation_optouts FOR INSERT TO authenticated 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 = conversation_optouts.tenant_id AND tm.status = 'active' ) ); DROP POLICY IF EXISTS "optouts: update" ON public.conversation_optouts; CREATE POLICY "optouts: update" ON public.conversation_optouts FOR UPDATE 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 = conversation_optouts.tenant_id AND tm.status = 'active' ) ); -- --------------------------------------------------------------------------- -- RLS: keywords -- --------------------------------------------------------------------------- ALTER TABLE public.conversation_optout_keywords ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "optout_kw: select" ON public.conversation_optout_keywords; CREATE POLICY "optout_kw: select" ON public.conversation_optout_keywords 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 = conversation_optout_keywords.tenant_id AND tm.status = 'active' ) ); DROP POLICY IF EXISTS "optout_kw: insert custom" ON public.conversation_optout_keywords; CREATE POLICY "optout_kw: insert custom" ON public.conversation_optout_keywords FOR INSERT TO authenticated WITH CHECK ( tenant_id IS NOT NULL AND is_system = false AND ( public.is_saas_admin() OR EXISTS ( SELECT 1 FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.tenant_id = conversation_optout_keywords.tenant_id AND tm.status = 'active' ) ) ); DROP POLICY IF EXISTS "optout_kw: update/delete custom" ON public.conversation_optout_keywords; CREATE POLICY "optout_kw: update/delete custom" ON public.conversation_optout_keywords FOR UPDATE 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 = conversation_optout_keywords.tenant_id AND tm.status = 'active' ) ) ); DROP POLICY IF EXISTS "optout_kw: delete custom" ON public.conversation_optout_keywords; CREATE POLICY "optout_kw: delete custom" ON public.conversation_optout_keywords FOR DELETE 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 = conversation_optout_keywords.tenant_id AND tm.status = 'active' ) ) ); -- ========================================================================== -- FIM DA MIGRACAO -- ==========================================================================