wacrm/supabase/migrations/027_notifications.sql

132 lines
4.7 KiB
PL/PgSQL

-- ============================================================
-- 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;
-- Full replica identity so realtime UPDATE payloads include old column
-- values. Without this, payload.old only carries the primary key, which
-- makes it impossible to derive whether a row was unread before the update.
ALTER TABLE notifications REPLICA IDENTITY FULL;
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);
-- Only read_at updates are meaningful from the client; restrict via a
-- column-level security policy so other fields cannot be rewritten.
CREATE POLICY notifications_update ON notifications FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Restrict to read_at column only at the column-privilege level so
-- clients cannot overwrite title, body, or other immutable fields.
REVOKE UPDATE ON notifications FROM authenticated;
GRANT UPDATE (read_at) ON notifications TO authenticated;
-- ============================================================
-- 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 $$;