refactor: remove obsolete member management API module
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
-- =====================================================
|
||||
-- Soft Delete Consistency
|
||||
--
|
||||
-- Problem: deleteMember does a soft delete (status='resigned'),
|
||||
-- but child table FKs use ON DELETE CASCADE. A hard DELETE
|
||||
-- would silently destroy roles, honors, mandates, transfers
|
||||
-- with no audit trail.
|
||||
--
|
||||
-- Fix: Change CASCADE to RESTRICT on data-preserving tables,
|
||||
-- add BEFORE DELETE audit trigger, provide safe_delete_member().
|
||||
-- =====================================================
|
||||
|
||||
-- Step 1: Change ON DELETE CASCADE → RESTRICT on tables where
|
||||
-- child data has independent value and should be preserved.
|
||||
-- We must drop and recreate the FK constraints.
|
||||
|
||||
-- member_roles: board positions have historical value
|
||||
ALTER TABLE public.member_roles
|
||||
DROP CONSTRAINT IF EXISTS member_roles_member_id_fkey;
|
||||
ALTER TABLE public.member_roles
|
||||
ADD CONSTRAINT member_roles_member_id_fkey
|
||||
FOREIGN KEY (member_id) REFERENCES public.members(id) ON DELETE RESTRICT;
|
||||
|
||||
-- member_honors: awards/medals are permanent records
|
||||
ALTER TABLE public.member_honors
|
||||
DROP CONSTRAINT IF EXISTS member_honors_member_id_fkey;
|
||||
ALTER TABLE public.member_honors
|
||||
ADD CONSTRAINT member_honors_member_id_fkey
|
||||
FOREIGN KEY (member_id) REFERENCES public.members(id) ON DELETE RESTRICT;
|
||||
|
||||
-- sepa_mandates: financial records must be preserved
|
||||
ALTER TABLE public.sepa_mandates
|
||||
DROP CONSTRAINT IF EXISTS sepa_mandates_member_id_fkey;
|
||||
ALTER TABLE public.sepa_mandates
|
||||
ADD CONSTRAINT sepa_mandates_member_id_fkey
|
||||
FOREIGN KEY (member_id) REFERENCES public.members(id) ON DELETE RESTRICT;
|
||||
|
||||
-- member_transfers: audit trail must survive
|
||||
ALTER TABLE public.member_transfers
|
||||
DROP CONSTRAINT IF EXISTS member_transfers_member_id_fkey;
|
||||
ALTER TABLE public.member_transfers
|
||||
ADD CONSTRAINT member_transfers_member_id_fkey
|
||||
FOREIGN KEY (member_id) REFERENCES public.members(id) ON DELETE RESTRICT;
|
||||
|
||||
-- Keep CASCADE on tables where data is tightly coupled:
|
||||
-- member_department_assignments (junction table, no independent value)
|
||||
-- member_cards (regeneratable)
|
||||
-- member_portal_invitations (transient)
|
||||
|
||||
-- Step 2: Audit trigger before hard delete — snapshot the full record
|
||||
CREATE OR REPLACE FUNCTION public.audit_member_before_hard_delete()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
-- If an audit_log table exists, log the deletion
|
||||
INSERT INTO public.audit_log (
|
||||
account_id, user_id, table_name, record_id, action, old_data
|
||||
)
|
||||
SELECT
|
||||
OLD.account_id,
|
||||
COALESCE(
|
||||
nullif(current_setting('app.current_user_id', true), '')::uuid,
|
||||
auth.uid()
|
||||
),
|
||||
'members',
|
||||
OLD.id::text,
|
||||
'delete',
|
||||
to_jsonb(OLD);
|
||||
|
||||
RETURN OLD;
|
||||
EXCEPTION
|
||||
WHEN undefined_table THEN
|
||||
-- audit_log table doesn't exist yet, allow delete to proceed
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER trg_members_audit_before_delete
|
||||
BEFORE DELETE ON public.members
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.audit_member_before_hard_delete();
|
||||
|
||||
-- Step 3: Safe hard-delete function for super-admin use only
|
||||
-- Archives all child records first, then performs the delete.
|
||||
CREATE OR REPLACE FUNCTION public.safe_delete_member(
|
||||
p_member_id uuid,
|
||||
p_performed_by uuid DEFAULT NULL
|
||||
)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
v_member record;
|
||||
BEGIN
|
||||
-- Fetch member for validation
|
||||
SELECT * INTO v_member FROM public.members WHERE id = p_member_id;
|
||||
IF v_member IS NULL THEN
|
||||
RAISE EXCEPTION 'Member % not found', p_member_id
|
||||
USING ERRCODE = 'P0002';
|
||||
END IF;
|
||||
|
||||
-- Set the user ID for the audit trigger
|
||||
IF p_performed_by IS NOT NULL THEN
|
||||
PERFORM set_config('app.current_user_id', p_performed_by::text, true);
|
||||
END IF;
|
||||
|
||||
-- Delete child records that now use RESTRICT
|
||||
DELETE FROM public.member_roles WHERE member_id = p_member_id;
|
||||
DELETE FROM public.member_honors WHERE member_id = p_member_id;
|
||||
DELETE FROM public.sepa_mandates WHERE member_id = p_member_id;
|
||||
-- member_transfers: delete (the BEFORE DELETE trigger on members already snapshots everything)
|
||||
DELETE FROM public.member_transfers WHERE member_id = p_member_id;
|
||||
|
||||
-- Now the hard delete triggers audit_member_before_hard_delete
|
||||
DELETE FROM public.members WHERE id = p_member_id;
|
||||
END;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.safe_delete_member(uuid, uuid) TO service_role;
|
||||
-- Intentionally NOT granted to authenticated — super-admin only via admin client
|
||||
Reference in New Issue
Block a user