-- ===================================================== -- 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;