151 lines
5.1 KiB
PL/PgSQL
151 lines
5.1 KiB
PL/PgSQL
-- =====================================================
|
|
-- SEPA Data Deduplication (Phase 1)
|
|
--
|
|
-- Problem: members table has inline SEPA fields (iban, bic,
|
|
-- account_holder, sepa_mandate_id, sepa_mandate_date,
|
|
-- sepa_mandate_status, sepa_mandate_sequence, sepa_bank_name)
|
|
-- AND a separate sepa_mandates table. sepa_mandate_id is text,
|
|
-- not a FK to sepa_mandates(id) which is uuid. Data diverges.
|
|
--
|
|
-- Fix: Add proper primary_mandate_id FK, migrate inline data
|
|
-- to sepa_mandates rows, rewrite RPCs to read from sepa_mandates.
|
|
-- Inline columns are kept read-only for backward compat (phase 2 drops them).
|
|
-- =====================================================
|
|
|
|
-- Step 1: Add proper FK column pointing to the primary mandate
|
|
ALTER TABLE public.members
|
|
ADD COLUMN IF NOT EXISTS primary_mandate_id uuid
|
|
REFERENCES public.sepa_mandates(id) ON DELETE SET NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS ix_members_primary_mandate
|
|
ON public.members(primary_mandate_id)
|
|
WHERE primary_mandate_id IS NOT NULL;
|
|
|
|
-- Step 2: For members with inline SEPA data but no sepa_mandates row, create one
|
|
DO $$
|
|
DECLARE
|
|
r record;
|
|
v_mandate_id uuid;
|
|
BEGIN
|
|
FOR r IN
|
|
SELECT m.id AS member_id, m.account_id,
|
|
m.iban, m.bic, m.account_holder,
|
|
m.first_name, m.last_name,
|
|
m.sepa_mandate_id, m.sepa_mandate_date,
|
|
m.sepa_mandate_status, m.sepa_mandate_reference,
|
|
m.sepa_mandate_sequence, m.sepa_bank_name
|
|
FROM public.members m
|
|
WHERE m.iban IS NOT NULL AND m.iban != ''
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM public.sepa_mandates sm WHERE sm.member_id = m.id
|
|
)
|
|
LOOP
|
|
INSERT INTO public.sepa_mandates (
|
|
member_id, account_id, mandate_reference, iban, bic,
|
|
account_holder, mandate_date, status, sequence, is_primary, notes
|
|
) VALUES (
|
|
r.member_id,
|
|
r.account_id,
|
|
COALESCE(NULLIF(r.sepa_mandate_reference, ''), NULLIF(r.sepa_mandate_id, ''), 'MIGRATED-' || r.member_id::text),
|
|
r.iban,
|
|
r.bic,
|
|
COALESCE(NULLIF(r.account_holder, ''), NULLIF(TRIM(COALESCE(r.first_name, '') || ' ' || COALESCE(r.last_name, '')), ''), 'Unbekannt'),
|
|
COALESCE(r.sepa_mandate_date, current_date),
|
|
COALESCE(r.sepa_mandate_status, 'pending'::public.sepa_mandate_status),
|
|
COALESCE(NULLIF(r.sepa_mandate_sequence, ''), 'RCUR'),
|
|
true,
|
|
CASE WHEN r.sepa_bank_name IS NOT NULL AND r.sepa_bank_name != ''
|
|
THEN 'Bank: ' || r.sepa_bank_name
|
|
ELSE NULL
|
|
END
|
|
)
|
|
RETURNING id INTO v_mandate_id;
|
|
|
|
UPDATE public.members SET primary_mandate_id = v_mandate_id WHERE id = r.member_id;
|
|
END LOOP;
|
|
END $$;
|
|
|
|
-- Step 3: For members that already have sepa_mandates rows, link the primary one
|
|
UPDATE public.members m
|
|
SET primary_mandate_id = sm.id
|
|
FROM public.sepa_mandates sm
|
|
WHERE sm.member_id = m.id
|
|
AND sm.is_primary = true
|
|
AND m.primary_mandate_id IS NULL;
|
|
|
|
-- If no mandate marked as primary, pick the most recent active one
|
|
UPDATE public.members m
|
|
SET primary_mandate_id = (
|
|
SELECT sm.id FROM public.sepa_mandates sm
|
|
WHERE sm.member_id = m.id
|
|
ORDER BY
|
|
CASE WHEN sm.status = 'active' THEN 0 ELSE 1 END,
|
|
sm.created_at DESC
|
|
LIMIT 1
|
|
)
|
|
WHERE m.primary_mandate_id IS NULL
|
|
AND EXISTS (SELECT 1 FROM public.sepa_mandates sm WHERE sm.member_id = m.id);
|
|
|
|
-- Step 4: Rewrite list_hierarchy_sepa_eligible_members to read from sepa_mandates
|
|
CREATE OR REPLACE FUNCTION public.list_hierarchy_sepa_eligible_members(
|
|
root_account_id uuid,
|
|
p_account_filter uuid DEFAULT NULL
|
|
)
|
|
RETURNS TABLE (
|
|
member_id uuid,
|
|
account_id uuid,
|
|
account_name varchar,
|
|
first_name text,
|
|
last_name text,
|
|
iban text,
|
|
bic text,
|
|
account_holder text,
|
|
mandate_id text,
|
|
mandate_date date,
|
|
dues_amount numeric
|
|
)
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
BEGIN
|
|
IF NOT public.has_role_on_account(root_account_id) THEN
|
|
RETURN;
|
|
END IF;
|
|
|
|
RETURN QUERY
|
|
SELECT
|
|
m.id AS member_id,
|
|
m.account_id,
|
|
a.name AS account_name,
|
|
m.first_name,
|
|
m.last_name,
|
|
sm.iban,
|
|
sm.bic,
|
|
sm.account_holder,
|
|
sm.mandate_reference AS mandate_id,
|
|
sm.mandate_date,
|
|
COALESCE(dc.amount, 0) AS dues_amount
|
|
FROM public.members m
|
|
JOIN public.accounts a ON a.id = m.account_id
|
|
JOIN public.sepa_mandates sm ON sm.id = m.primary_mandate_id
|
|
LEFT JOIN public.dues_categories dc ON dc.id = m.dues_category_id
|
|
WHERE m.account_id IN (SELECT d FROM public.get_account_descendants(root_account_id) d)
|
|
AND m.status = 'active'
|
|
AND sm.iban IS NOT NULL
|
|
AND sm.status = 'active'
|
|
AND (p_account_filter IS NULL OR m.account_id = p_account_filter)
|
|
ORDER BY a.name, m.last_name, m.first_name;
|
|
END;
|
|
$$;
|
|
|
|
-- Step 5: Add partial index for fast SEPA-eligible lookups
|
|
CREATE INDEX IF NOT EXISTS ix_sepa_mandates_active_primary
|
|
ON public.sepa_mandates(member_id)
|
|
WHERE status = 'active' AND is_primary = true;
|
|
|
|
-- Note: Inline SEPA columns (iban, bic, account_holder, sepa_mandate_id,
|
|
-- sepa_mandate_date, sepa_mandate_status, sepa_mandate_sequence, sepa_bank_name)
|
|
-- are kept for read-only backward compatibility. Phase 2 migration will drop them
|
|
-- after all code paths are migrated to use sepa_mandates via primary_mandate_id.
|