Commit Graph

4 Commits

Author SHA1 Message Date
wmj 9eb2ce7ce3 Per-member estate model: each family member authors their own Wassiyah/Waqf,
mutawalli executes on any member's trigger, heir email notification

Per explicit product direction: "all members of the family can make their
own wassiyah or waqif. The mutawali or the trustee agent can access and
execute those wassiyah and waqif upon any event triggers. Warith or the
heir will be automatically notified via email."

Schema: nf_family_members.role now includes 'member' (authors own documents,
doesn't manage the family). nf_wassiyah_settings/nf_wassiyah_bequests/
nf_waqf_designations gained author_id — each is now per-author, not
per-family. Added recipient_email / beneficiary_email columns for warith
notification targets. New nf_member_triggers (composite PK family_id+
member_id) and nf_member_attestors: a per-member death trigger, separate
from the legacy family-wide nf_death_triggers (kept for backward
compatibility, still exercised by existing suites).

RLS: any family member can READ any other member's Wassiyah/Waqf (the
mutawalli needs full visibility to execute), but only the document's own
author can WRITE to it — not even the owner. Firing a member's trigger
requires the caller to have role agent/owner AND not be the member
themselves (enforced in the policy's WITH CHECK, not just the UI) — matches
"the mutawalli executes, never for themselves."

New UI: FamilyManagement gained a role selector (member vs agent) on
invites. New MutawalliDashboard.svelte — the trustee's execution surface:
pick any family member, see their Wassiyah/Waqf read-only, set up
attestors + death cert ref, fire their trigger (blocked for self both by
disabled UI and by RLS), then trigger heir email notifications.

Edge Function notify-heirs deployed (Deno, uses Resend): reads the
triggered member's Wassiyah recipients and Waqf beneficiaries wherever an
email was recorded, sends each a notice. Returns a clear 501 rather than
failing silently until RESEND_API_KEY is set as a project secret.

Three real bugs found via testing against the live backend, not visible
from code review alone:
- listFamilyMembers() never selected user_id — every member-scoped lookup
  on the new dashboard was silently keying off undefined.
- nf_member_triggers keyed by member_id alone: since a person can belong
  to multiple families, firing a trigger in one family marked them
  "triggered" in every other family they belong to. Fixed to composite
  (family_id, member_id) key.
- Classic Svelte 5 $state pitfall: (proxyObject[key] ??= []).push(item)
  mutates the plain array literal the ??= expression evaluates to, not
  the proxy-wrapped array Svelte actually tracks — so pushed items were
  silently invisible to the UI forever. Fixed by building on a plain
  object and assigning to the $state variable once. Also found and fixed
  the same design smell in the older WassiyahGenerator/FamilyWaqfDesignator
  authorId handling: it was snapshotted once via currentUser()?.id at
  mount instead of read live off the session store, which could silently
  break writes on a remount that happened before session hydration
  finished — now reads live and guards refresh() on it being present.

CoverageDashboard and DeathTrigger updated to check ANY family member's
Waqf corpus for coverage (not just one author's), since coverage is a
family-wide view even though authorship is per-member now.

e2e-per-member.cjs: new suite covering the full flow — owner invites a
member and an agent; member authors a private Wassiyah (invisible to
other members, confirming per-author isolation); mutawalli sees it on
their dashboard and fires the member's trigger; member cannot fire their
own; heir notification call completes with either Sent or a clear
"not configured" failure, never hangs. 11/11 passing.

Full regression sweep after these changes: e2e-uat 32/32 (stable across
3 consecutive runs), e2e-fastpath 16/16, e2e-trust 12/12, e2e-business
10/10, e2e-digital-vehicle 10/10, e2e-property 9/9, e2e-other 4/4,
e2e-info 31/31, e2e-family-agent 12/12 (updated for the new invite-form
role selector), e2e-per-member 11/11 — 178/178 total, no regressions.
2026-08-14 05:51:46 +08:00
wmj b9a98bd97a Restore all 8 pre-auth E2E suites with a sign-in prelude; fix bugs they found
New e2e-auth-helper.cjs: shared signInFreshFamily() prelude — signs in as
the confirmed test owner account and creates a uniquely-named family per
run, so accumulated data from a previous run's assets/hibah/etc. (now
persisted in Supabase, not wiped with the browser context like localStorage
was) can never bleed into another run's percentage/coverage assertions.
All 8 suites (e2e-uat, e2e-fastpath, e2e-trust, e2e-business,
e2e-digital-vehicle, e2e-property, e2e-other, e2e-info) now call it in
place of the old anonymous page.goto(BASE).

Restoring them surfaced two real product bugs, not just test staleness:

1. WassiyahGenerator.svelte was never migrated to Supabase in the earlier
   backend work — it still called the old local storage.js load()/save()
   for assets, bequests, and witnesses, so the one-third meter silently
   read an empty local cache and always showed 0. Migrated to family-scoped
   Supabase tables (new nf_wassiyah_bequests, nf_wassiyah_settings, with
   member-scoped RLS) matching the pattern used for Hibah/Nominations/etc.

2. storage.js's exportAll() did an unguarded JSON.parse on every
   "nf."-prefixed localStorage key, but family.js stores activeFamilyId as
   a raw string (not JSON-encoded) — one malformed parse threw and silently
   aborted the whole export before the file download fired. Made exportAll
   defensive: falls back to the raw string on a parse failure instead of
   throwing.

The remaining test failures were async-timing gaps inherent to the move
from synchronous localStorage reads to async Supabase fetches: several
assertions checked <select> option counts or newly-created rows immediately
after a fixed short wait, before the async load/refresh had actually
landed. Fixed by replacing blind isVisible()/fixed-timeout checks with
proper waitFor()/polling in the test helpers (selectByText, corpus-select
population, row-creation checks) — not a product bug, but worth fixing
since the old timing assumptions no longer hold now that data is live and
shared instead of instant and local.

Results: e2e-uat 32/32, e2e-fastpath 16/16, e2e-trust 12/12,
e2e-business 10/10, e2e-digital-vehicle 10/10, e2e-property 9/9,
e2e-other 4/4, e2e-info 31/31 — 124/124. Re-verified e2e-family-agent
(12/12) and e2e-smoke-authed (24/24) still pass after the
WassiyahGenerator migration. 160/160 total across all ten suites.
2026-08-13 21:30:46 +08:00
wmj 3badc5caaa Fix E2E test bugs (regex selectOption, tab context) — full suite now 32/32 green 2026-08-13 17:09:23 +08:00
wmj eb1ce59159 Wire language switcher into Settings; add E2E UAT suite
- App.svelte: language toggle (EN/BM) wired to existing i18n.js store,
  header tagline reflects selection. Was previously dead code with no
  UI control (found by e2e-uat.cjs during full-flow UAT).
- e2e-uat.cjs: Playwright-driven E2E UAT against the live deployment —
  real clicks/typing through all 7 tabs, faraid textbook-case
  verification, heir-exclusion blocking, marad al-mawt guard, 1/3 cap
  override flow, claim issuance/transfer, export/delete-all guards,
  PWA and console-error checks.
2026-08-13 17:08:41 +08:00