feat(fase-10): CU-H08 dissolve collective — RPC + danger-zone UI

Migration 018 adds public.dissolve_collective(uuid):
- admin-only; non-admin (member/guest/non-member) → errcode P0002
- single DELETE from public.collectives; all child tables cascade
  (collective_members, collective_invitations, shopping_lists →
  shopping_items, task_lists → tasks, notes, item_frequency,
  sync_conflicts — verified by audit, no schema change needed)

/collective/manage gains a "Danger zone" card (admin-only) with a
Dissolve button. The confirmation modal loads live counts of lists,
tasks and notes, displays them in the warning, and requires the user
to type the collective name exactly before the confirm button is
enabled. On success, locals stores drop the collective and the user
is sent to /lists (next collective) or /onboarding (none left).

pgTAP 011 (8 assertions): RPC exists, member/guest/non-member all
rejected with P0002, admin succeeds, collective row gone, child
list cascade-deleted, members cascade-deleted. Playwright D-01..D-03
cover the happy path, the type-the-name guard, and member visibility.

Auxiliary collective fixture (NOT the seed) per test — the seed
collective must survive every run for downstream suites.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 02:11:21 +02:00
parent 5e2ba3a6d6
commit 83f613ec1e
4 changed files with 402 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
-- Migration 018: dissolve_collective(uuid) RPC — Fase 10.3 (CU-H08).
--
-- Permanently delete a collective and all its content. Admin-only. Errcode
-- P0002 so the UI can distinguish "you're not admin" from generic errors.
--
-- All content tables that reference public.collectives.collective_id are
-- already ON DELETE CASCADE (collective_members, collective_invitations,
-- shopping_lists → shopping_items, task_lists → tasks, notes, item_frequency,
-- sync_conflicts). Deleting the parent row cleans up the entire subtree.
-- See plan/fase-10-spec-completion.md §10.3 for the audit + Riesgo 2.
CREATE OR REPLACE FUNCTION public.dissolve_collective(p_collective_id uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, auth
AS $$
DECLARE
v_user uuid := auth.uid();
v_role public.member_role;
BEGIN
IF v_user IS NULL THEN
RAISE EXCEPTION 'not authenticated' USING ERRCODE = '28000';
END IF;
SELECT role INTO v_role
FROM public.collective_members
WHERE collective_id = p_collective_id AND user_id = v_user;
IF v_role IS DISTINCT FROM 'admin' THEN
RAISE EXCEPTION 'only admins can dissolve a collective'
USING ERRCODE = 'P0002';
END IF;
DELETE FROM public.collectives WHERE id = p_collective_id;
END;
$$;
REVOKE ALL ON FUNCTION public.dissolve_collective(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.dissolve_collective(uuid) TO authenticated;