145 lines
4.9 KiB
PL/PgSQL
145 lines
4.9 KiB
PL/PgSQL
-- =====================================================
|
|
-- Member Communications Tracking
|
|
--
|
|
-- Records all communications with/about members:
|
|
-- emails sent, phone calls, notes, letters, meetings.
|
|
-- Communications are append-only for authenticated users.
|
|
-- Only service_role (admin) can delete.
|
|
-- Integrates with audit log via triggers.
|
|
-- =====================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS public.member_communications (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
member_id uuid NOT NULL REFERENCES public.members(id) ON DELETE CASCADE,
|
|
account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
|
|
type text NOT NULL CHECK (type IN ('email', 'phone', 'letter', 'meeting', 'note', 'sms')),
|
|
direction text NOT NULL DEFAULT 'outbound' CHECK (direction IN ('inbound', 'outbound', 'internal')),
|
|
subject text CHECK (subject IS NULL OR length(subject) <= 500),
|
|
body text CHECK (body IS NULL OR length(body) <= 50000),
|
|
-- Email-specific fields
|
|
email_to text,
|
|
email_cc text,
|
|
email_message_id text,
|
|
-- Attachment references (Supabase Storage paths)
|
|
attachment_paths text[] CHECK (attachment_paths IS NULL OR array_length(attachment_paths, 1) <= 10),
|
|
-- Audit
|
|
created_by uuid NOT NULL REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
COMMENT ON TABLE public.member_communications IS
|
|
'Communication log per member — emails, calls, notes, letters, meetings. Append-only for regular users.';
|
|
|
|
CREATE INDEX ix_member_comms_member
|
|
ON public.member_communications(member_id, created_at DESC);
|
|
CREATE INDEX ix_member_comms_account
|
|
ON public.member_communications(account_id, created_at DESC);
|
|
CREATE INDEX ix_member_comms_type
|
|
ON public.member_communications(account_id, type);
|
|
|
|
ALTER TABLE public.member_communications ENABLE ROW LEVEL SECURITY;
|
|
REVOKE ALL ON public.member_communications FROM authenticated, service_role;
|
|
-- Append-only: authenticated users can SELECT + INSERT, not UPDATE/DELETE
|
|
GRANT SELECT, INSERT ON public.member_communications TO authenticated;
|
|
GRANT ALL ON public.member_communications TO service_role;
|
|
|
|
-- Read: must have a role on the account
|
|
CREATE POLICY member_comms_select
|
|
ON public.member_communications FOR SELECT TO authenticated
|
|
USING (public.has_role_on_account(account_id));
|
|
|
|
-- Insert: must have members.write permission
|
|
CREATE POLICY member_comms_insert
|
|
ON public.member_communications FOR INSERT TO authenticated
|
|
WITH CHECK (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions));
|
|
|
|
-- No UPDATE/DELETE policies for authenticated — communications are immutable
|
|
-- service_role can still delete via admin client when necessary
|
|
|
|
-- Auto-log to audit trail on communication INSERT
|
|
CREATE OR REPLACE FUNCTION public.trg_member_comm_audit_insert()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
BEGIN
|
|
INSERT INTO public.member_audit_log (
|
|
member_id, account_id, user_id, action, metadata
|
|
) VALUES (
|
|
NEW.member_id,
|
|
NEW.account_id,
|
|
NEW.created_by,
|
|
'communication_logged',
|
|
jsonb_build_object(
|
|
'communication_id', NEW.id,
|
|
'type', NEW.type,
|
|
'direction', NEW.direction,
|
|
'subject', NEW.subject
|
|
)
|
|
);
|
|
RETURN NEW;
|
|
EXCEPTION WHEN OTHERS THEN
|
|
-- Audit failure should not block the insert
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
CREATE TRIGGER trg_member_comms_audit_insert
|
|
AFTER INSERT ON public.member_communications
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.trg_member_comm_audit_insert();
|
|
|
|
-- Safe delete function for admin use — logs before deleting
|
|
CREATE OR REPLACE FUNCTION public.delete_member_communication(
|
|
p_communication_id uuid,
|
|
p_account_id uuid
|
|
)
|
|
RETURNS void
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
DECLARE
|
|
v_comm record;
|
|
BEGIN
|
|
-- Verify caller has access
|
|
IF NOT public.has_permission(auth.uid(), p_account_id, 'members.write'::public.app_permissions) THEN
|
|
RAISE EXCEPTION 'Access denied' USING ERRCODE = '42501';
|
|
END IF;
|
|
|
|
-- Fetch the communication for audit
|
|
SELECT * INTO v_comm
|
|
FROM public.member_communications
|
|
WHERE id = p_communication_id AND account_id = p_account_id;
|
|
|
|
IF v_comm IS NULL THEN
|
|
RAISE EXCEPTION 'Communication not found' USING ERRCODE = 'P0002';
|
|
END IF;
|
|
|
|
-- Log deletion to audit trail
|
|
INSERT INTO public.member_audit_log (
|
|
member_id, account_id, user_id, action, metadata
|
|
) VALUES (
|
|
v_comm.member_id,
|
|
v_comm.account_id,
|
|
auth.uid(),
|
|
'communication_logged',
|
|
jsonb_build_object(
|
|
'deleted_communication_id', v_comm.id,
|
|
'type', v_comm.type,
|
|
'direction', v_comm.direction,
|
|
'subject', v_comm.subject,
|
|
'action_detail', 'deleted'
|
|
)
|
|
);
|
|
|
|
-- Delete via service_role context (SECURITY DEFINER bypasses RLS)
|
|
DELETE FROM public.member_communications
|
|
WHERE id = p_communication_id AND account_id = p_account_id;
|
|
END;
|
|
$$;
|
|
|
|
GRANT EXECUTE ON FUNCTION public.delete_member_communication(uuid, uuid)
|
|
TO authenticated, service_role;
|