-- ============================================================================= -- Migration: 20260420000006_conv_messages_notifications -- Sessao 11 - Fase 5a (extensao). -- -- Integra conversation_messages ao sistema de notifications existente: -- - Adiciona 'inbound_message' ao CHECK do notifications.type -- - Trigger em conversation_messages: quando chega inbound, fan-out para -- members do tenant apropriados (responsible_member do paciente, ou -- todos tenant_admin/clinic_admin/therapist ativos se sem vinculo) -- ============================================================================= -- Ajusta CHECK do type ALTER TABLE public.notifications DROP CONSTRAINT IF EXISTS notifications_type_check; ALTER TABLE public.notifications ADD CONSTRAINT notifications_type_check CHECK (type = ANY (ARRAY[ 'new_scheduling', 'new_patient', 'recurrence_alert', 'session_status', 'inbound_message' ])); -- --------------------------------------------------------------------------- -- Trigger function: fan-out mensagem inbound para notifications dos members -- --------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION public.fanout_inbound_message_to_notifications() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$ DECLARE v_target_user UUID; v_title TEXT; v_detail TEXT; v_initials TEXT; v_deeplink TEXT; v_patient_name TEXT; v_payload JSONB; BEGIN -- so processa inbound IF NEW.direction <> 'inbound' THEN RETURN NEW; END IF; -- busca nome do paciente (se vinculado) IF NEW.patient_id IS NOT NULL THEN SELECT nome_completo INTO v_patient_name FROM public.patients WHERE id = NEW.patient_id; END IF; -- titulo e detalhes v_title := COALESCE(v_patient_name, NEW.from_number, 'Desconhecido'); v_detail := COALESCE(left(NEW.body, 100), '[mensagem sem texto]'); -- iniciais IF v_patient_name IS NOT NULL THEN v_initials := upper(left(v_patient_name, 1)) || COALESCE(upper(left(split_part(v_patient_name, ' ', 2), 1)), ''); ELSE v_initials := '?'; END IF; -- deeplink para a pagina de conversas (clinic padrao; therapist tambem funciona via mesma rota na area dele) v_deeplink := '/admin/conversas'; v_payload := jsonb_build_object( 'title', v_title, 'detail', v_detail, 'avatar_initials', v_initials, 'deeplink', v_deeplink, 'channel', NEW.channel, 'conversation_message_id', NEW.id, 'patient_id', NEW.patient_id, 'from_number', NEW.from_number ); -- ─── decide destinatarios ───────────────────────────────────────────── -- Caso 1: paciente vinculado e tem responsible_member_id IF NEW.patient_id IS NOT NULL THEN SELECT tm.user_id INTO v_target_user FROM public.patients p JOIN public.tenant_members tm ON tm.id = p.responsible_member_id WHERE p.id = NEW.patient_id AND tm.status = 'active' LIMIT 1; IF v_target_user IS NOT NULL THEN INSERT INTO public.notifications (owner_id, tenant_id, type, ref_id, ref_table, payload) VALUES (v_target_user, NEW.tenant_id, 'inbound_message', NULL, 'conversation_messages', v_payload); RETURN NEW; END IF; END IF; -- Caso 2: fallback — fan-out pra todos tenant_admin/clinic_admin/therapist ativos INSERT INTO public.notifications (owner_id, tenant_id, type, ref_id, ref_table, payload) SELECT tm.user_id, NEW.tenant_id, 'inbound_message', NULL, 'conversation_messages', v_payload FROM public.tenant_members tm WHERE tm.tenant_id = NEW.tenant_id AND tm.status = 'active' AND tm.role IN ('clinic_admin', 'tenant_admin', 'therapist'); RETURN NEW; END; $$; -- Trigger DROP TRIGGER IF EXISTS trg_fanout_inbound_to_notifications ON public.conversation_messages; CREATE TRIGGER trg_fanout_inbound_to_notifications AFTER INSERT ON public.conversation_messages FOR EACH ROW EXECUTE FUNCTION public.fanout_inbound_message_to_notifications(); COMMENT ON FUNCTION public.fanout_inbound_message_to_notifications() IS 'Cria registros em notifications pra members apropriados quando chega mensagem inbound. Respeita responsible_member do paciente.';