-- ===================================================== -- Event-Member Linkage -- -- Problem: event_registrations links to members by email -- only. If a member changes their email, event history is -- lost. transfer_member matches by email — fragile. -- -- Fix: Add member_id FK to event_registrations, backfill -- from email matches, update transfer_member. -- ===================================================== -- Add member_id FK column ALTER TABLE public.event_registrations ADD COLUMN IF NOT EXISTS member_id uuid REFERENCES public.members(id) ON DELETE SET NULL; CREATE INDEX IF NOT EXISTS ix_event_registrations_member ON public.event_registrations(member_id) WHERE member_id IS NOT NULL; -- Backfill: match existing registrations to members by email within the same account UPDATE public.event_registrations er SET member_id = m.id FROM public.events e, public.members m WHERE e.id = er.event_id AND m.account_id = e.account_id AND lower(m.email) = lower(er.email) AND m.email IS NOT NULL AND m.email != '' AND m.status IN ('active', 'inactive', 'pending') AND er.member_id IS NULL AND er.email IS NOT NULL AND er.email != ''; -- Update transfer_member to count active events via member_id instead of email CREATE OR REPLACE FUNCTION public.transfer_member( p_member_id uuid, p_target_account_id uuid, p_reason text DEFAULT NULL, p_keep_sepa boolean DEFAULT true ) RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$ DECLARE v_source_account_id uuid; v_source_name varchar; v_target_name varchar; v_active_courses bigint; v_active_events bigint; v_cleared_data jsonb; v_transfer_id uuid; v_member record; BEGIN -- Get current member state SELECT * INTO v_member FROM public.members WHERE id = p_member_id; IF v_member IS NULL THEN RAISE EXCEPTION 'Member not found'; END IF; v_source_account_id := v_member.account_id; -- Verify target account exists IF NOT EXISTS (SELECT 1 FROM public.accounts WHERE id = p_target_account_id) THEN RAISE EXCEPTION 'Target account not found'; END IF; -- Ensure caller has access to source account IF NOT public.has_role_on_account_or_ancestor(v_source_account_id) THEN RAISE EXCEPTION 'Access denied to source account'; END IF; -- Same account? No-op IF v_source_account_id = p_target_account_id THEN RAISE EXCEPTION 'Cannot transfer member to the same account'; END IF; -- Ensure both accounts share a common ancestor IF NOT EXISTS ( SELECT 1 FROM public.get_account_ancestors(v_source_account_id) sa JOIN public.get_account_ancestors(p_target_account_id) ta ON sa = ta ) THEN RAISE EXCEPTION 'Source and target accounts do not share a common ancestor (Verband)'; END IF; -- Get org names for the transfer note SELECT name INTO v_source_name FROM public.accounts WHERE id = v_source_account_id; SELECT name INTO v_target_name FROM public.accounts WHERE id = p_target_account_id; -- Count active relationships (informational, for the log) SELECT count(*) INTO v_active_courses FROM public.course_participants cp JOIN public.courses c ON c.id = cp.course_id WHERE cp.member_id = p_member_id AND cp.status = 'enrolled'; -- Use member_id for event lookups instead of fragile email matching SELECT count(*) INTO v_active_events FROM public.event_registrations er JOIN public.events e ON e.id = er.event_id WHERE er.member_id = p_member_id AND er.status IN ('confirmed', 'pending') AND e.event_date >= current_date; -- Perform the transfer UPDATE public.members SET account_id = p_target_account_id, -- Clear org-specific admin data dues_category_id = NULL, member_number = NULL, -- Clear primary_mandate_id FK (mandate needs re-confirmation in new org) primary_mandate_id = NULL, -- Legacy inline SEPA fields (deprecated, kept for backward compat) sepa_mandate_id = CASE WHEN p_keep_sepa THEN sepa_mandate_id ELSE NULL END, sepa_mandate_date = CASE WHEN p_keep_sepa THEN sepa_mandate_date ELSE NULL END, sepa_mandate_status = 'pending', -- Append transfer note notes = COALESCE(notes, '') || E'\n[Transfer ' || to_char(now(), 'YYYY-MM-DD') || '] ' || v_source_name || ' → ' || v_target_name || COALESCE(' | Grund: ' || p_reason, ''), is_transferred = true WHERE id = p_member_id; -- Reset SEPA mandate(s) in the mandates table UPDATE public.sepa_mandates SET status = 'pending' WHERE member_id = p_member_id AND status = 'active'; -- Build cleared data snapshot for the transfer log v_cleared_data := jsonb_build_object( 'member_number', v_member.member_number, 'dues_category_id', v_member.dues_category_id, 'active_courses', v_active_courses, 'active_events', v_active_events ); -- Create transfer log entry INSERT INTO public.member_transfers ( member_id, source_account_id, target_account_id, transferred_by, reason, cleared_data ) VALUES ( p_member_id, v_source_account_id, p_target_account_id, COALESCE(nullif(current_setting('app.current_user_id', true), '')::uuid, auth.uid()), p_reason, v_cleared_data ) RETURNING id INTO v_transfer_id; RETURN v_transfer_id; END; $$;