diff --git a/COMPETITIVE_BENCHMARK.md b/COMPETITIVE_BENCHMARK.md index 3b4a2ce..bd94f71 100644 --- a/COMPETITIVE_BENCHMARK.md +++ b/COMPETITIVE_BENCHMARK.md @@ -62,8 +62,15 @@ differentiators: spouse. Ja'fari (Shia) is deliberately gated as unsupported rather than silently computed with Sunni rules, since it uses a structurally different classification system, not a parameter tweak. -4. **No human-in-the-loop / professional tier — open.** Every competitor pairs software with a - lawyer or consultant; Nur Falah remains self-serve only. +4. **Human-in-the-loop / professional tier — closed, as a review-request workflow.** Not a + marketplace or payment integration (out of scope) — a structured status on each member's + Wassiyah (Not reviewed → Review requested → Reviewed, with reviewer name recorded), surfaced + on the Mutawalli dashboard so nothing quietly ships as final without the legally-required + review being flagged. Pairs with a new whole-app jurisdiction setting (Malaysia/Singapore/UK, + owner-configurable in Settings) that drives jurisdiction-specific legal notes in both the + Wassiyah tab and the generated draft will document (Wills Act 1959/state Syariah for + Malaysia, Wills Act 1838 + AMLA for Singapore, Wills Act 1837 for the UK) and Zakat + currency/Nisab defaults. 5. **AI guidance layer — closed.** Coverage Dashboard now surfaces a rule-based recommendations list (exposed assets, missing Wassiyah, unverified assets, unlinked liabilities, no insurance logged, no Zakat set up, insufficient attestors, no agent diff --git a/e2e-jurisdiction-review.cjs b/e2e-jurisdiction-review.cjs new file mode 100644 index 0000000..6506e19 --- /dev/null +++ b/e2e-jurisdiction-review.cjs @@ -0,0 +1,113 @@ +// Verifies the whole-app jurisdiction setting (MY/SG/UK), its downstream +// effects on Wassiyah/will document/Zakat currency, and the professional +// review request workflow against the live backend. +const { chromium } = require('playwright'); +const { signInFreshFamily } = require('./e2e-auth-helper.cjs'); +const BASE = 'https://moslem04.falahos.my/'; +const results = []; +const consoleErrors = []; +function record(name, pass, detail = '') { results.push({ name, pass, detail }); console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); } + +async function main() { + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); + page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); }); + page.on('pageerror', e => consoleErrors.push(e.message)); + + await signInFreshFamily(page, BASE, 'e2e-jurisdiction'); + + // ── Settings: jurisdiction selector, owner can change ── + await page.locator('nav button[aria-label="Settings"]').click(); + await page.waitForTimeout(600); + const jurisdictionSelectVisible = await page.locator('.jurisdiction-setting select').isVisible().catch(() => false); + record('Settings: jurisdiction selector visible for owner', jurisdictionSelectVisible); + + await page.locator('.jurisdiction-setting select').selectOption('SG'); + await page.waitForTimeout(1000); + const savedBadge = await page.locator('.jurisdiction-setting .saved').isVisible().catch(() => false); + record('Settings: changing jurisdiction shows a saved confirmation', savedBadge); + + // ── Zakat: currency defaults follow the family jurisdiction (SGD) ── + await page.locator('nav button[aria-label="Zakat"]').click(); + await page.waitForTimeout(800); + const sgdLabelVisible = await page.locator('.field:has-text("Cash")').textContent(); + record('Zakat: currency label reflects Singapore jurisdiction (SGD)', sgdLabelVisible.includes('SGD'), sgdLabelVisible); + const nisabValue = await page.locator('.field:has-text("Nisab") input').inputValue(); + record('Zakat: Nisab default follows Singapore jurisdiction (8000)', nisabValue === '8000', nisabValue); + + // Log an asset first so the one-third cap isn't zero (a fresh family with + // no assets has cap=0, which would make any bequest below "exceed" it). + await page.locator('nav button[aria-label="Assets"]').click(); + await page.waitForTimeout(500); + await page.locator('.form-card .field:has-text("Description") input').fill('Savings'); + await page.locator('.form-card .field:has-text("Estimated value") input').fill('30000'); + await page.locator('.form-card button.btn-primary', { hasText: 'Add asset' }).click(); + await page.waitForTimeout(1000); + + // ── Wassiyah: jurisdiction dropdown includes Singapore, defaults from family setting ── + await page.locator('nav button[aria-label="Wassiyah"]').click(); + await page.waitForTimeout(800); + const wassiyahJurisdiction = await page.locator('.field:has-text("Jurisdiction") select').inputValue(); + record('Wassiyah: jurisdiction defaults to the family setting (SG)', wassiyahJurisdiction === 'SG', wassiyahJurisdiction); + const sgOptionVisible = await page.locator('.field:has-text("Jurisdiction") select option[value="SG"]').count(); + record('Wassiyah: Singapore is a selectable jurisdiction option', sgOptionVisible === 1); + + // ── Draft will document reflects Singapore-specific legal notes ── + await page.locator('.field:has-text("Full legal name") input').fill('Siti binte Rahman'); + await page.locator('.field:has-text("Recipient name") input').fill('Local Mosque Fund'); + await page.locator('.field:has-text("Relation to you") input').fill('charity'); + await page.locator('.field:has-text("Description") input').fill('Cash gift'); + await page.locator('.field:has-text("Value") input').first().fill('3000'); + await page.locator('button.btn-primary', { hasText: 'Add bequest' }).click(); + await page.waitForTimeout(1000); + + const [docPage] = await Promise.all([ + page.waitForEvent('popup'), + page.locator('button.btn-secondary', { hasText: 'Generate draft will document' }).click() + ]); + await docPage.waitForLoadState(); + const docText = await docPage.locator('body').innerText(); + record('Will document: Singapore-specific legal note present (Wills Act 1838 / AMLA)', docText.includes('1838') && docText.includes('AMLA'), docText.slice(0, 50)); + await docPage.close(); + + // ── Professional review request workflow ── + const notRequestedVisible = await page.locator('.review-card', { hasText: "hasn't been reviewed" }).isVisible().catch(() => false); + record('Wassiyah: starts as not-yet-reviewed', notRequestedVisible); + + await page.locator('button.btn-secondary', { hasText: 'Request professional review' }).click(); + await page.waitForTimeout(1000); + const requestedVisible = await page.locator('.review-card.review-requested').isVisible().catch(() => false); + record('Wassiyah: requesting review updates status', requestedVisible); + + await page.locator('.reviewer-row input').fill('Ahmad & Co Solicitors'); + await page.locator('.reviewer-row button', { hasText: 'Mark reviewed' }).click(); + await page.waitForTimeout(1000); + const reviewedVisible = await page.locator('.review-card.review-reviewed', { hasText: 'Ahmad & Co Solicitors' }).isVisible().catch(() => false); + record('Wassiyah: marking reviewed records the reviewer name', reviewedVisible); + + // Review status surfaces on the Mutawalli dashboard (owner-only family — owner sees own row) + await page.locator('nav button[aria-label="Mutawalli"]').click(); + await page.waitForTimeout(800); + const mutawalliReviewBadge = await page.locator('.review-badge.review-reviewed').isVisible().catch(() => false); + const mutawalliGated = (await page.locator('.module').innerText()).includes('Only the mutawalli'); + record('Mutawalli: review status surfaces on the dashboard (or family is correctly gated)', mutawalliReviewBadge || mutawalliGated); + + // ── Isolation: a different family (same account) is unaffected by this jurisdiction change ── + const isolationPage = await browser.newPage({ viewport: { width: 390, height: 844 } }); + await signInFreshFamily(isolationPage, BASE, 'e2e-jurisdiction-isolation'); + await isolationPage.locator('nav button[aria-label="Settings"]').click(); + await isolationPage.waitForTimeout(600); + const isolatedJurisdiction = await isolationPage.locator('.jurisdiction-setting select').inputValue(); + record('Isolation: a different family defaults to MY, unaffected by the SG change above', isolatedJurisdiction === 'MY', isolatedJurisdiction); + await isolationPage.close(); + + record('No uncaught JS console errors during full session', consoleErrors.length === 0, consoleErrors.join(' || ')); + + await browser.close(); + const passCount = results.filter(r => r.pass).length; + const failCount = results.length - passCount; + console.log(`\n${passCount} passed, ${failCount} failed, ${results.length} total`); + if (failCount > 0) results.filter(r => !r.pass).forEach(r => console.log(` - ${r.name}: ${r.detail}`)); + process.exit(failCount > 0 ? 1 : 0); +} +main().catch(e => { console.error('SCRIPT ERROR:', e); process.exit(2); }); diff --git a/src/App.svelte b/src/App.svelte index 13af60f..86a5463 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -20,6 +20,7 @@ import FamilyTree from './lib/FamilyTree.svelte'; import InsurancePolicies from './lib/InsurancePolicies.svelte'; import ZakatCalculator from './lib/ZakatCalculator.svelte'; + import JurisdictionSetting from './lib/JurisdictionSetting.svelte'; let currentLang = $state('en'); lang.subscribe(v => currentLang = v); @@ -125,6 +126,8 @@
Signed in as {currentSession.user.email}.
+