Migration 017 adds public.leave_collective(uuid) with three branches: - normal member: remove membership row - sole admin with other members: promote oldest-joined remaining member to admin (inline; the existing user-delete trigger does not cover the membership-delete path), then remove the row - sole member: reject with errcode P0001 so the UI can direct the user to the dissolve flow (CU-H08) Settings page gains a "Your collectives" section listing the user's memberships with a per-row Leave button; the confirmation modal calls the RPC, drops the collective from local stores, and either switches to the next collective or sends the user to /onboarding when none remain. Also seeds the Danger zone scaffolding for Fase 10.5 and adds all message keys consumed by 10.1, 10.3, 10.5 and 10.6. pgTAP 010 (7 assertions): member-leave, sole-admin-leave + auto-promote, sole-member rejected with P0001. Playwright L-01 walks Borja through the UI flow + checks the seed collective survives (content stays). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
85 lines
2.9 KiB
PL/PgSQL
85 lines
2.9 KiB
PL/PgSQL
-- Migration 017: leave_collective(uuid) RPC — Fase 10.1 (CU-H06).
|
|
--
|
|
-- A user abandons one of their collectives. The content stays.
|
|
--
|
|
-- If the user is the only admin and other members exist, the existing
|
|
-- `promote_oldest_member_on_admin_leave` trigger fires on DELETE FROM
|
|
-- public.users — but that trigger only runs on user-account delete, not
|
|
-- on collective_members delete. So we replicate the auto-promote logic
|
|
-- inline before removing the membership row.
|
|
--
|
|
-- If the user is the sole member of the collective, leaving is disallowed:
|
|
-- they must dissolve instead (CU-H08, migration 018). Errcode P0001 lets
|
|
-- the UI distinguish this case from generic errors and link to the
|
|
-- dissolve flow.
|
|
|
|
CREATE OR REPLACE FUNCTION public.leave_collective(p_collective_id uuid)
|
|
RETURNS void
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public, auth
|
|
AS $$
|
|
DECLARE
|
|
v_user uuid := auth.uid();
|
|
v_member_count int;
|
|
v_is_admin boolean;
|
|
v_other_admins int;
|
|
BEGIN
|
|
IF v_user IS NULL THEN
|
|
RAISE EXCEPTION 'not authenticated' USING ERRCODE = '28000';
|
|
END IF;
|
|
|
|
-- Guard: caller must actually be a member of the collective.
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM public.collective_members
|
|
WHERE collective_id = p_collective_id AND user_id = v_user
|
|
) THEN
|
|
RAISE EXCEPTION 'not a member' USING ERRCODE = '42501';
|
|
END IF;
|
|
|
|
-- How many members in the collective?
|
|
SELECT count(*) INTO v_member_count
|
|
FROM public.collective_members
|
|
WHERE collective_id = p_collective_id;
|
|
|
|
IF v_member_count = 1 THEN
|
|
-- Sole member — must dissolve, not leave.
|
|
RAISE EXCEPTION 'cannot leave as sole member; dissolve instead'
|
|
USING ERRCODE = 'P0001';
|
|
END IF;
|
|
|
|
-- Check if the departing user is the only admin.
|
|
SELECT role = 'admin' INTO v_is_admin
|
|
FROM public.collective_members
|
|
WHERE collective_id = p_collective_id AND user_id = v_user;
|
|
|
|
IF v_is_admin THEN
|
|
SELECT count(*) INTO v_other_admins
|
|
FROM public.collective_members
|
|
WHERE collective_id = p_collective_id
|
|
AND user_id <> v_user
|
|
AND role = 'admin';
|
|
|
|
-- If sole admin, promote the oldest remaining (non-departing) member.
|
|
IF v_other_admins = 0 THEN
|
|
UPDATE public.collective_members
|
|
SET role = 'admin'
|
|
WHERE (collective_id, user_id) = (
|
|
SELECT collective_id, user_id
|
|
FROM public.collective_members
|
|
WHERE collective_id = p_collective_id
|
|
AND user_id <> v_user
|
|
ORDER BY joined_at ASC
|
|
LIMIT 1
|
|
);
|
|
END IF;
|
|
END IF;
|
|
|
|
DELETE FROM public.collective_members
|
|
WHERE collective_id = p_collective_id AND user_id = v_user;
|
|
END;
|
|
$$;
|
|
|
|
REVOKE ALL ON FUNCTION public.leave_collective(uuid) FROM PUBLIC;
|
|
GRANT EXECUTE ON FUNCTION public.leave_collective(uuid) TO authenticated;
|