-- ========================================================================== -- Agencia PSI — Migracao: Auto-reply fora do horario (CRM Grupo 2.3) -- ========================================================================== -- Criado por: Leonardo Nohama -- Data: 2026-04-21 · Sao Carlos/SP — Brasil -- -- Quando paciente manda mensagem fora do horario de atendimento, dispara -- resposta automatica configuravel. Anti-spam via cooldown por thread. -- -- Modos de horario: -- - 'agenda' → usa agenda_regras_semanais dos membros do tenant -- - 'business_hours' → janela semanal do tenant (clinica inteira) -- - 'custom' → janela semanal especifica deste auto-reply -- -- business_hours e custom_window usam mesma estrutura JSONB: -- [{ "dow": 0-6, "start": "HH:MM", "end": "HH:MM" }, ...] -- Multiplas entradas por dia permitidas (ex: 08:00-12:00 + 13:00-18:00) -- ========================================================================== -- --------------------------------------------------------------------------- -- Settings per-tenant -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.conversation_autoreply_settings ( tenant_id UUID PRIMARY KEY REFERENCES public.tenants(id) ON DELETE CASCADE, enabled BOOLEAN NOT NULL DEFAULT false, message TEXT NOT NULL DEFAULT 'Olá! Nosso horário de atendimento acabou. Retornaremos sua mensagem assim que possível. Obrigado!' CHECK (length(message) > 0 AND length(message) <= 2000), cooldown_minutes INT NOT NULL DEFAULT 180 CHECK (cooldown_minutes >= 0 AND cooldown_minutes <= 43200), -- 0 min a 30 dias schedule_mode TEXT NOT NULL DEFAULT 'agenda' CHECK (schedule_mode IN ('agenda', 'business_hours', 'custom')), -- Janela de funcionamento da clinica (reutilizavel por outras features) -- Ex: [{ "dow": 1, "start": "08:00", "end": "18:00" }, ...] business_hours JSONB NOT NULL DEFAULT '[]'::jsonb, -- Janela especifica deste auto-reply (quando schedule_mode = 'custom') custom_window JSONB NOT NULL DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); DROP TRIGGER IF EXISTS trg_conv_autoreply_settings_updated_at ON public.conversation_autoreply_settings; CREATE TRIGGER trg_conv_autoreply_settings_updated_at BEFORE UPDATE ON public.conversation_autoreply_settings FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); COMMENT ON TABLE public.conversation_autoreply_settings IS 'Configuracao por tenant do auto-reply fora do horario.'; -- --------------------------------------------------------------------------- -- Log (anti-spam: uma resposta auto por thread por cooldown) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.conversation_autoreply_log ( id BIGSERIAL PRIMARY KEY, tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, thread_key TEXT NOT NULL, sent_at TIMESTAMPTZ NOT NULL DEFAULT now(), message_id UUID -- referencia opcional pra message na tabela conversation_messages ); CREATE INDEX IF NOT EXISTS idx_autoreply_log_cooldown ON public.conversation_autoreply_log (tenant_id, thread_key, sent_at DESC); COMMENT ON TABLE public.conversation_autoreply_log IS 'Log de auto-replies enviados. Usado pra respeitar cooldown por thread.'; -- --------------------------------------------------------------------------- -- RLS: settings -- --------------------------------------------------------------------------- ALTER TABLE public.conversation_autoreply_settings ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "autoreply_settings: select" ON public.conversation_autoreply_settings; CREATE POLICY "autoreply_settings: select" ON public.conversation_autoreply_settings 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_autoreply_settings.tenant_id AND tm.status = 'active' ) ); DROP POLICY IF EXISTS "autoreply_settings: insert" ON public.conversation_autoreply_settings; CREATE POLICY "autoreply_settings: insert" ON public.conversation_autoreply_settings 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_autoreply_settings.tenant_id AND tm.status = 'active' ) ); DROP POLICY IF EXISTS "autoreply_settings: update" ON public.conversation_autoreply_settings; CREATE POLICY "autoreply_settings: update" ON public.conversation_autoreply_settings 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_autoreply_settings.tenant_id AND tm.status = 'active' ) ); -- --------------------------------------------------------------------------- -- RLS: log (read-only pra tenant members; escrita via service_role) -- --------------------------------------------------------------------------- ALTER TABLE public.conversation_autoreply_log ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "autoreply_log: select" ON public.conversation_autoreply_log; CREATE POLICY "autoreply_log: select" ON public.conversation_autoreply_log 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_autoreply_log.tenant_id AND tm.status = 'active' ) ); -- ========================================================================== -- FIM DA MIGRACAO -- ==========================================================================