156 lines
5.1 KiB
PL/PgSQL
156 lines
5.1 KiB
PL/PgSQL
-- =====================================================
|
|
-- 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
|
|
JOIN public.members m ON 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')
|
|
WHERE e.id = er.event_id
|
|
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 false
|
|
)
|
|
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;
|
|
$$;
|