feat: close final competitive gap — professional review workflow + multi-country (MY/SG/UK)

Closes the 4th and final gap from the competitive benchmark: the
human-in-the-loop professional tier every commercial competitor pairs
with their software. Deliberately NOT a marketplace or payment
integration — a structured review-request status on each member's
Wassiyah (not_requested -> requested -> reviewed, with reviewer name
recorded), surfaced on the Mutawalli dashboard so nothing quietly ships
as final without the legally-required review being flagged.

Paired with a new whole-app jurisdiction setting since 'necessary in
certain countries' only makes sense per-jurisdiction:

- nf_families.jurisdiction (MY/SG/UK), owner-only to change — first
  UPDATE policy ever added to nf_families, which previously had none.
- New Settings-tab jurisdiction selector (JurisdictionSetting.svelte).
- Wassiyah tab and the draft will document now carry jurisdiction-
  specific legal notes: Wills Act 1959 + state Syariah (Malaysia),
  Wills Act 1838 + AMLA (Singapore), Wills Act 1837 (UK). Singapore
  added as a full third jurisdiction option, not just a stub.
- Zakat calculator's currency label and Nisab default now follow the
  family's jurisdiction (RM/SGD/GBP) instead of a hardcoded RM guess.

Covered by e2e-jurisdiction-review.cjs (13/13). Full regression:
253/253 across all suites (2 reruns confirmed as pre-existing
parallel-load flakes, not regressions).
This commit is contained in:
2026-08-14 12:37:25 +08:00
parent 71aa4ae47c
commit 97a1d2ebe2
11 changed files with 327 additions and 31 deletions
+23
View File
@@ -234,6 +234,29 @@ export async function listAllWassiyahForFamily(familyId) {
return data || [];
}
// ── Professional review workflow — a structured status, not a marketplace.
// Any family member can request review of their own Wassiyah; anyone (in
// practice the mutawalli/agent, coordinating with an actual lawyer outside
// the app) can mark it reviewed once done.
export async function requestWassiyahReview(familyId, authorId) {
const { error } = await supabase.from('nf_wassiyah_settings').upsert({
family_id: familyId, author_id: authorId, review_status: 'requested', requested_at: new Date().toISOString(), updated_at: new Date().toISOString()
}, { onConflict: 'family_id,author_id' });
if (error) throw error;
}
export async function markWassiyahReviewed(familyId, authorId, reviewerName) {
const { error } = await supabase.from('nf_wassiyah_settings').upsert({
family_id: familyId, author_id: authorId, review_status: 'reviewed', reviewer_name: reviewerName, reviewed_at: new Date().toISOString(), updated_at: new Date().toISOString()
}, { onConflict: 'family_id,author_id' });
if (error) throw error;
}
/** For the mutawalli dashboard: every family member's Wassiyah settings (jurisdiction, review status). */
export async function listAllWassiyahSettingsForFamily(familyId) {
const { data, error } = await supabase.from('nf_wassiyah_settings').select('*').eq('family_id', familyId);
if (error) throw error;
return data || [];
}
// ── Per-member death trigger — the mutawalli/owner fires it for a specific
// member, never the member themselves (enforced by RLS, not just the UI). ──
export async function getMemberTrigger(memberId, familyId) {