Records the closing state of Fase 10: spec coverage now 100% on the H-series CUs, full FK on-delete audit table, 5 architectural decisions (Keycloak not cleaned up on account delete, sole-member must dissolve, type-the-name dissolve confirmation, client-side language detection, trash via RPC). Bumps the MVP2 progress to 2/6 and the test totals to 360 active + 3 skipped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
177 lines
8.2 KiB
Markdown
177 lines
8.2 KiB
Markdown
# Fase 10 — Spec completion (MVP2 2/6)
|
|
|
|
**Status: ✅ complete · 2026-05-18.**
|
|
|
|
Closes the 8 functional-spec gaps audited on 2026-04-22:
|
|
|
|
1. CU-H06 Abandonar colectivo
|
|
2. CU-H07 Renombrar colectivo + cambiar emoji
|
|
3. CU-H08 Disolver colectivo
|
|
4. Trash drawer — restore + hard delete
|
|
5. Eliminación de cuenta (hard delete)
|
|
6. "Crear nuevo colectivo" desde la sidebar
|
|
7. Botón "Reiniciar lista" en vista de detalle
|
|
8. Detección automática de idioma (Accept-Language)
|
|
|
|
Spec coverage on the CU-H\* axis (collective management + user lifecycle)
|
|
is now 100 %.
|
|
|
|
---
|
|
|
|
## Migrations introduced
|
|
|
|
| # | File | Purpose |
|
|
|---|---|---|
|
|
| 017 | `017_leave_collective_rpc.sql` | `public.leave_collective(uuid)` — sole-admin auto-promote inline; P0001 for sole-member |
|
|
| 018 | `018_dissolve_collective_rpc.sql` | `public.dissolve_collective(uuid)` — admin only (P0002), cascade-driven |
|
|
| 019 | `019_user_deletion_fks.sql` | Relax every `created_by` / `updated_by` FK from `NOT NULL RESTRICT` → `NULL SET NULL` |
|
|
| 020 | `020_restore_list_rpc.sql` | `public.restore_list(uuid)` + `public.hard_delete_list(uuid)` with explicit role guards |
|
|
| 021 | `021_delete_account_rpc.sql` | `public.delete_account()` — two-step (`public.users` then `auth.users`); P0003 for sole-admin-stuck |
|
|
|
|
Migration **019** was held back from the trash work (10.4) and used here in 10.5 — that's why
|
|
the trash RPC migration is 020, leaving 019 as the FK-normalization seat.
|
|
|
|
---
|
|
|
|
## FK on-delete audit (Fase 10.5.1)
|
|
|
|
Before migration 019, deleting a `public.users` row would have been blocked
|
|
by every table where the user had ever created content. The relaxed
|
|
behaviour after 019:
|
|
|
|
| Table | Column | Before | After |
|
|
|---|---|---|---|
|
|
| `collectives` | `created_by` | `NOT NULL RESTRICT` | `NULL SET NULL` |
|
|
| `collective_members` | `user_id` | `NOT NULL CASCADE` | (unchanged — membership belongs to the user) |
|
|
| `collective_invitations` | `created_by` | `NOT NULL CASCADE` | `NULL SET NULL` |
|
|
| `collective_invitations` | `accepted_by` | `NULL SET NULL` | (unchanged) |
|
|
| `shopping_lists` | `created_by` | `NOT NULL RESTRICT` | `NULL SET NULL` |
|
|
| `shopping_items` | `created_by` | `NOT NULL RESTRICT` | `NULL SET NULL` |
|
|
| `shopping_items` | `checked_by` | `NULL SET NULL` | (unchanged) |
|
|
| `task_lists` | `created_by` | `NOT NULL RESTRICT` | `NULL SET NULL` |
|
|
| `tasks` | `created_by` | `NOT NULL RESTRICT` | `NULL SET NULL` |
|
|
| `tasks` | `completed_by` | `NULL SET NULL` | (unchanged) |
|
|
| `notes` | `created_by` | `NOT NULL RESTRICT` | `NULL SET NULL` |
|
|
| `notes` | `updated_by` | `NOT NULL RESTRICT` | `NULL SET NULL` |
|
|
| `sync_conflicts` | `user_id` | `NOT NULL CASCADE (→ auth.users)` | (unchanged — log entries vanish with the user) |
|
|
|
|
Implication: any UI that renders the author of a row must defensively
|
|
handle `NULL`. The intended label is "Deleted user" — the visual polish
|
|
is deferred to a later cosmetic pass; the data layer is correct now.
|
|
|
|
---
|
|
|
|
## Decisions documented
|
|
|
|
1. **Keycloak is NOT cleaned up on `delete_account()`.** The Keycloak
|
|
admin API requires an admin token (separate auth realm), and is the
|
|
external operator's domain. After `delete_account()` the row is gone
|
|
from GoTrue's `auth.users` and the identity link in `auth.identities`,
|
|
but the Keycloak account itself survives. The next OIDC login with
|
|
the same email reaches GoTrue with no matching identity, GoTrue
|
|
provisions a new `auth.users` row with a fresh UUID, the
|
|
`handle_new_user` trigger creates the corresponding `public.users`
|
|
row, and the user is effectively a brand-new profile. The settings
|
|
modal makes this explicit.
|
|
|
|
2. **Sole-member "leave" is rejected**, not auto-converted to dissolve.
|
|
`leave_collective` raises errcode P0001 in that case; the UI surfaces
|
|
the "use Dissolve instead" hint with no follow-up navigation. This
|
|
keeps the destructive paths explicit (per plan §Riesgo 4).
|
|
|
|
3. **Dissolve confirmation by typing the collective name**, not a
|
|
checkbox — pattern from GitHub/Vercel for destructive operations.
|
|
Plan §10.3 confirmed; D-02 enforces it.
|
|
|
|
4. **Auto-language detection lives client-side** (`navigator.languages`)
|
|
not server-side (`Accept-Language` header). The OIDC dance (browser
|
|
↔ Keycloak ↔ GoTrue) bypasses SvelteKit entirely, so a
|
|
`hooks.server.ts` would never see those requests. The bootstrap is
|
|
gated to the post-`SIGNED_IN` path on the layout, fires only once
|
|
per account (60-second-old + `language='en'` heuristic), and skips
|
|
any user who has explicitly set a language before. Hispanic-locale
|
|
users land in Spanish without touching anything; existing users are
|
|
not retroactively re-detected.
|
|
|
|
5. **Trash UI uses RPCs even though the existing PostgREST UPDATE/DELETE
|
|
already works.** The RPC route closes a `is_active_member` ambiguity
|
|
(guest is excluded today via WITH CHECK, but the surface area was
|
|
broader than the two intents) and pre-emptively flags the case where
|
|
someone tries to hard-delete a still-active list. Direct UPDATEs
|
|
continue to pass C-07..C-12 in `rls-lists.test.ts` — the policies
|
|
are unchanged.
|
|
|
|
---
|
|
|
|
## Test deltas
|
|
|
|
| Suite | Baseline (Fase 9) | After Fase 10 | Δ |
|
|
|---|---|---|---|
|
|
| pgTAP | 65 | 96 | +31 (010 leave +7, 011 dissolve +8, 012 trash-rpcs +8, 013 delete-account +8) |
|
|
| Vitest integration | 144 | 151 | +7 (trash-rpcs +5, language-bootstrap +2) |
|
|
| Vitest unit | 26 | 38 | +12 (accept-language) |
|
|
| Playwright E2E | 65 | 74 | +9 (L-01, MC-01a/b, D-01/02/03, O-04, RST-01, DEL-01) |
|
|
| Rate-limit (gated) | 1 | 1 | — |
|
|
| **Total active** | **301** | **360** | **+59** |
|
|
| Skipped | 3 | 3 | — |
|
|
|
|
All suites green at end of fase. RLS audit clean.
|
|
|
|
---
|
|
|
|
## Files added
|
|
|
|
* `supabase/migrations/017_leave_collective_rpc.sql`
|
|
* `supabase/migrations/018_dissolve_collective_rpc.sql`
|
|
* `supabase/migrations/019_user_deletion_fks.sql`
|
|
* `supabase/migrations/020_restore_list_rpc.sql`
|
|
* `supabase/migrations/021_delete_account_rpc.sql`
|
|
* `supabase/tests/010_leave_collective.sql`
|
|
* `supabase/tests/011_dissolve_collective.sql`
|
|
* `supabase/tests/012_trash_rpcs.sql`
|
|
* `supabase/tests/013_delete_account.sql`
|
|
* `apps/web/src/lib/utils/accept-language.ts`
|
|
* `apps/web/src/lib/utils/accept-language.test.ts`
|
|
* `apps/web/src/lib/components/CreateCollectiveModal.svelte`
|
|
* `apps/web/tests/e2e/leave-collective.test.ts`
|
|
* `apps/web/tests/e2e/rename-collective.test.ts`
|
|
* `apps/web/tests/e2e/dissolve-collective.test.ts`
|
|
* `apps/web/tests/e2e/account-deletion.test.ts`
|
|
* `apps/web/tests/e2e/sidebar-create-collective.test.ts`
|
|
* `apps/web/tests/e2e/reset-from-detail.test.ts`
|
|
* `packages/test-utils/tests/trash-rpcs.test.ts`
|
|
* `packages/test-utils/tests/language-bootstrap.test.ts`
|
|
|
|
## Files modified
|
|
|
|
* `apps/web/src/routes/(app)/settings/+page.svelte` — Collectives section, Danger zone, delete-account modal
|
|
* `apps/web/src/routes/(app)/collective/manage/+page.svelte` — rename + emoji picker, Dissolve danger zone, dissolve confirm modal
|
|
* `apps/web/src/routes/(app)/lists/[id]/+page.svelte` — prominent Reset button on the completed-list view
|
|
* `apps/web/src/lib/stores/lists.ts` — restore/hard-delete go through the new RPCs
|
|
* `apps/web/src/lib/components/layout/DesktopSidebar.svelte` — switcher always-open, "+ New collective" entry
|
|
* `apps/web/src/lib/components/layout/MobileDrawer.svelte` — matching "+ New collective" entry
|
|
* `apps/web/src/routes/+layout.svelte` — `maybeBootstrapLanguage()` after SIGNED_IN
|
|
* `apps/web/messages/en.json` + `apps/web/messages/es.json` — ~30 new keys
|
|
|
|
---
|
|
|
|
## Spec coverage table (analysis/analisis-funcional.md)
|
|
|
|
| CU | Description | Status before | Status after |
|
|
|---|---|---|---|
|
|
| CU-H01 | Crear cuenta | ✅ | ✅ |
|
|
| CU-H02 | Crear colectivo | ✅ (onboarding only) | ✅ + sidebar |
|
|
| CU-H03 | Invitar miembros | ✅ | ✅ |
|
|
| CU-H04 | Aceptar invitación | ✅ | ✅ |
|
|
| CU-H05 | Cambiar de colectivo | ✅ | ✅ |
|
|
| **CU-H06** | **Abandonar colectivo** | ❌ | **✅** |
|
|
| **CU-H07** | **Renombrar / cambiar emoji** | ❌ | **✅** |
|
|
| **CU-H08** | **Disolver colectivo** | ❌ | **✅** |
|
|
| §6.3 | Eliminación de cuenta | ❌ | **✅** |
|
|
| §5.1 | Trash restore | partial (no UI) | **✅** |
|
|
| §3.4 | Reset list from detail | partial (overview only) | **✅** |
|
|
| §6.2 | Accept-Language detection | ❌ | **✅** |
|
|
|
|
All H-series CUs and the audited completion-debt items are now covered
|
|
end-to-end.
|