- ============================================================ -- NOTIFICATIONS -- ============================================================ CREATE TABLE IF NOT EXISTS notifications ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, -- Recipient — the agent this notification is for. user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, type TEXT NOT NULL DEFAULT 'conversation_assigned' CHECK (type IN ('conversation_assigned')), conversation_id UUID REFERENCES conversations(id) ON DELETE CASCADE, contact_id UUID REFERENCES contacts(id) ON DELETE SET NULL, -- Who triggered the notification. NULL means an automation / the -- system did it rather than a signed-in teammate. actor_user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, title TEXT NOT NULL, body TEXT, read_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_notifications_user_created ON notifications(user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_notifications_user_unread ON notifications(user_id) WHERE read_at IS NULL; ALTER TABLE notifications ENABLE ROW LEVEL SECURITY; -- Recipients can read and mark their own notifications as read. -- No client INSERT/DELETE policy — rows are created exclusively by -- the SECURITY DEFINER trigger function below. DROP POLICY IF EXISTS notifications_select ON notifications; DROP POLICY IF EXISTS notifications_update ON notifications; CREATE POLICY notifications_select ON notifications FOR SELECT USING (auth.uid() = user_id); CREATE POLICY notifications_update ON notifications FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); -- ============================================================ -- TRIGGER — notify on conversation assignment -- ============================================================ CREATE OR REPLACE FUNCTION notify_conversation_assigned() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_contact_name TEXT; v_actor_name TEXT; BEGIN IF TG_OP = 'INSERT' THEN IF NEW.assigned_agent_id IS NULL THEN RETURN NEW; END IF; ELSE IF NEW.assigned_agent_id IS NULL OR NEW.assigned_agent_id IS NOT DISTINCT FROM OLD.assigned_agent_id THEN RETURN NEW; END IF; END IF; -- Skip self-assignment — nothing to notify the agent about. IF auth.uid() IS NOT NULL AND auth.uid() = NEW.assigned_agent_id THEN RETURN NEW; END IF; SELECT COALESCE(NULLIF(name, ''), phone) INTO v_contact_name FROM contacts WHERE id = NEW.contact_id; IF auth.uid() IS NOT NULL THEN SELECT full_name INTO v_actor_name FROM profiles WHERE user_id = auth.uid(); END IF; INSERT INTO notifications ( account_id, user_id, type, conversation_id, contact_id, actor_user_id, title, body ) VALUES ( NEW.account_id, NEW.assigned_agent_id, 'conversation_assigned', NEW.id, NEW.contact_id, auth.uid(), 'New conversation assigned', COALESCE(v_actor_name, 'Someone') || ' assigned you a conversation with ' || COALESCE(v_contact_name, 'a contact') ); RETURN NEW; EXCEPTION WHEN OTHERS THEN -- Never let a notification failure block the assignment itself. RAISE WARNING 'Failed to create assignment notification for conversation %: %', NEW.id, SQLERRM; RETURN NEW; END; $$; ALTER FUNCTION notify_conversation_assigned() OWNER TO postgres; DROP TRIGGER IF EXISTS on_conversation_assigned ON conversations; CREATE TRIGGER on_conversation_assigned AFTER INSERT OR UPDATE OF assigned_agent_id ON conversations FOR EACH ROW EXECUTE FUNCTION notify_conversation_assigned(); -- ============================================================ -- ENABLE REALTIME -- ============================================================ DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_publication_tables WHERE pubname = 'supabase_realtime' AND tablename = 'notifications' ) THEN ALTER PUBLICATION supabase_realtime ADD TABLE notifications; END IF; END $$;