/* * ------------------------------------------------------- * Member Management Parity Migration * Adds missing columns, departments, roles, honors, * SEPA mandates table, dues extensions, duplicate detection * ------------------------------------------------------- */ -- ===================================================== -- A1. Add missing columns to members table -- ===================================================== ALTER TABLE public.members ADD COLUMN IF NOT EXISTS salutation text; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS street2 text; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS phone2 text; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS fax text; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS birthplace text; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS birth_country text DEFAULT 'DE'; -- Membership lifecycle flags ALTER TABLE public.members ADD COLUMN IF NOT EXISTS is_honorary boolean NOT NULL DEFAULT false; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS is_founding_member boolean NOT NULL DEFAULT false; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS is_youth boolean NOT NULL DEFAULT false; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS is_retiree boolean NOT NULL DEFAULT false; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS is_probationary boolean NOT NULL DEFAULT false; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS is_transferred boolean NOT NULL DEFAULT false; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS is_archived boolean NOT NULL DEFAULT false; -- Youth/Guardian ALTER TABLE public.members ADD COLUMN IF NOT EXISTS guardian_name text; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS guardian_phone text; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS guardian_email text; -- Dues tracking ALTER TABLE public.members ADD COLUMN IF NOT EXISTS dues_year integer; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS dues_paid boolean NOT NULL DEFAULT false; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS additional_fees numeric(10,2) DEFAULT 0; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS exemption_type text; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS exemption_reason text; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS exemption_amount numeric(10,2); -- SEPA extras ALTER TABLE public.members ADD COLUMN IF NOT EXISTS sepa_mandate_reference text; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS sepa_mandate_sequence text DEFAULT 'RCUR'; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS sepa_bank_name text; -- GDPR granular ALTER TABLE public.members ADD COLUMN IF NOT EXISTS gdpr_newsletter boolean NOT NULL DEFAULT false; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS gdpr_internet boolean NOT NULL DEFAULT false; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS gdpr_print boolean NOT NULL DEFAULT false; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS gdpr_birthday_info boolean NOT NULL DEFAULT false; -- Online portal ALTER TABLE public.members ADD COLUMN IF NOT EXISTS online_access_key text; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS online_access_blocked boolean NOT NULL DEFAULT false; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS email_confirmed boolean NOT NULL DEFAULT false; -- Address quality ALTER TABLE public.members ADD COLUMN IF NOT EXISTS address_invalid boolean NOT NULL DEFAULT false; ALTER TABLE public.members ADD COLUMN IF NOT EXISTS data_reconciliation_needed boolean NOT NULL DEFAULT false; -- ===================================================== -- A2. member_departments + assignments -- ===================================================== CREATE TABLE IF NOT EXISTS public.member_departments ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE, name text NOT NULL, description text, sort_order integer NOT NULL DEFAULT 0, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS ix_member_departments_account ON public.member_departments(account_id); ALTER TABLE public.member_departments ENABLE ROW LEVEL SECURITY; REVOKE ALL ON public.member_departments FROM authenticated, service_role; GRANT SELECT, INSERT, UPDATE, DELETE ON public.member_departments TO authenticated; GRANT ALL ON public.member_departments TO service_role; CREATE POLICY member_departments_select ON public.member_departments FOR SELECT TO authenticated USING (public.has_role_on_account(account_id)); CREATE POLICY member_departments_mutate ON public.member_departments FOR ALL TO authenticated USING (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions)); CREATE TABLE IF NOT EXISTS public.member_department_assignments ( member_id uuid NOT NULL REFERENCES public.members(id) ON DELETE CASCADE, department_id uuid NOT NULL REFERENCES public.member_departments(id) ON DELETE CASCADE, PRIMARY KEY (member_id, department_id) ); ALTER TABLE public.member_department_assignments ENABLE ROW LEVEL SECURITY; REVOKE ALL ON public.member_department_assignments FROM authenticated, service_role; GRANT SELECT, INSERT, DELETE ON public.member_department_assignments TO authenticated; GRANT ALL ON public.member_department_assignments TO service_role; CREATE POLICY mda_select ON public.member_department_assignments FOR SELECT TO authenticated USING (EXISTS (SELECT 1 FROM public.members m WHERE m.id = member_department_assignments.member_id AND public.has_role_on_account(m.account_id))); CREATE POLICY mda_mutate ON public.member_department_assignments FOR ALL TO authenticated USING (EXISTS (SELECT 1 FROM public.members m WHERE m.id = member_department_assignments.member_id AND public.has_permission(auth.uid(), m.account_id, 'members.write'::public.app_permissions))); -- ===================================================== -- A3. member_roles (board positions / Funktionen) -- ===================================================== CREATE TABLE IF NOT EXISTS public.member_roles ( 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, role_name text NOT NULL, from_date date, until_date date, is_active boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS ix_member_roles_member ON public.member_roles(member_id); ALTER TABLE public.member_roles ENABLE ROW LEVEL SECURITY; REVOKE ALL ON public.member_roles FROM authenticated, service_role; GRANT SELECT, INSERT, UPDATE, DELETE ON public.member_roles TO authenticated; GRANT ALL ON public.member_roles TO service_role; CREATE POLICY member_roles_select ON public.member_roles FOR SELECT TO authenticated USING (public.has_role_on_account(account_id)); CREATE POLICY member_roles_mutate ON public.member_roles FOR ALL TO authenticated USING (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions)); -- ===================================================== -- A4. member_honors (Ehrungen) -- ===================================================== CREATE TABLE IF NOT EXISTS public.member_honors ( 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, honor_name text NOT NULL, honor_date date, description text, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS ix_member_honors_member ON public.member_honors(member_id); ALTER TABLE public.member_honors ENABLE ROW LEVEL SECURITY; REVOKE ALL ON public.member_honors FROM authenticated, service_role; GRANT SELECT, INSERT, UPDATE, DELETE ON public.member_honors TO authenticated; GRANT ALL ON public.member_honors TO service_role; CREATE POLICY member_honors_select ON public.member_honors FOR SELECT TO authenticated USING (public.has_role_on_account(account_id)); CREATE POLICY member_honors_mutate ON public.member_honors FOR ALL TO authenticated USING (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions)); -- ===================================================== -- A5. sepa_mandates (proper sub-table) -- ===================================================== CREATE TABLE IF NOT EXISTS public.sepa_mandates ( 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, mandate_reference text NOT NULL, iban text NOT NULL, bic text, account_holder text NOT NULL, mandate_date date NOT NULL, status public.sepa_mandate_status NOT NULL DEFAULT 'active', sequence text NOT NULL DEFAULT 'RCUR' CHECK (sequence IN ('FRST','RCUR','FNAL','OOFF')), is_primary boolean NOT NULL DEFAULT true, has_error boolean NOT NULL DEFAULT false, last_used_at timestamptz, notes text, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS ix_sepa_mandates_member ON public.sepa_mandates(member_id); CREATE INDEX IF NOT EXISTS ix_sepa_mandates_account ON public.sepa_mandates(account_id); ALTER TABLE public.sepa_mandates ENABLE ROW LEVEL SECURITY; REVOKE ALL ON public.sepa_mandates FROM authenticated, service_role; GRANT SELECT, INSERT, UPDATE, DELETE ON public.sepa_mandates TO authenticated; GRANT ALL ON public.sepa_mandates TO service_role; CREATE POLICY sepa_mandates_select ON public.sepa_mandates FOR SELECT TO authenticated USING (public.has_role_on_account(account_id)); CREATE POLICY sepa_mandates_mutate ON public.sepa_mandates FOR ALL TO authenticated USING (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions)); CREATE TRIGGER trg_sepa_mandates_updated_at BEFORE UPDATE ON public.sepa_mandates FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp(); -- ===================================================== -- A6. Extend dues_categories -- ===================================================== ALTER TABLE public.dues_categories ADD COLUMN IF NOT EXISTS is_youth boolean NOT NULL DEFAULT false; ALTER TABLE public.dues_categories ADD COLUMN IF NOT EXISTS is_exit boolean NOT NULL DEFAULT false; -- ===================================================== -- A7. Duplicate detection function -- ===================================================== CREATE OR REPLACE FUNCTION public.check_duplicate_member( p_account_id uuid, p_first_name text, p_last_name text, p_date_of_birth date DEFAULT NULL ) RETURNS TABLE(id uuid, member_number text, first_name text, last_name text, date_of_birth date, status public.membership_status) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$ SELECT m.id, m.member_number, m.first_name, m.last_name, m.date_of_birth, m.status FROM public.members m WHERE m.account_id = p_account_id AND lower(m.first_name) = lower(p_first_name) AND lower(m.last_name) = lower(p_last_name) AND (p_date_of_birth IS NULL OR m.date_of_birth = p_date_of_birth); $$; GRANT EXECUTE ON FUNCTION public.check_duplicate_member(uuid, text, text, date) TO authenticated, service_role;