diff --git a/e2e-fastpath.cjs b/e2e-fastpath.cjs new file mode 100644 index 0000000..c47ee60 --- /dev/null +++ b/e2e-fastpath.cjs @@ -0,0 +1,127 @@ +// Targeted E2E for the new fast-path mechanism: Coverage, Hibah asset-link, +// Waqf corpus-link, Nomination, Death Trigger. Run after the general e2e-uat.cjs. +const { chromium } = require('playwright'); +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 page.goto(BASE, { waitUntil: 'networkidle' }); + + const clickTab = async label => { await page.locator('nav button.tab', { hasText: label }).click(); await page.waitForTimeout(200); }; + + // New tabs exist + for (const label of ['Coverage', 'Nominate', 'Trigger']) { + const visible = await page.locator('nav button.tab', { hasText: label }).isVisible(); + record(`Nav: "${label}" tab exists`, visible); + } + + // Add an asset + await clickTab('Assets'); + await page.locator('.field:has-text("Description") input').fill('Land parcel, Perak'); + await page.locator('.field:has-text("Estimated value") input').fill('400000'); + await page.locator('button.btn-primary', { hasText: 'Add asset' }).click(); + await page.waitForTimeout(200); + + // Coverage should show 0% before any fast-path instrument + await clickTab('Coverage'); + const pctBefore = await page.locator('.big-percent').textContent(); + record('Coverage: shows 0% before any fast-path instrument attached', pctBefore.trim() === '0%', pctBefore); + const warningVisible = await page.locator('.warning-box').isVisible(); + record('Coverage: warning box shown when exposure > 0', warningVisible); + + // Link the asset via Hibah + await clickTab('Hibah'); + await page.locator('.field:has-text("Recipient") input').fill('My Nephew'); + await page.locator('.field:has-text("Relation to you") input').fill('nephew'); + await page.locator('.field:has-text("Asset / gift description") input').fill('Land parcel gift'); + const linkSelect = page.locator('select').first(); + await linkSelect.selectOption({ index: 1 }); + await page.locator('button.btn-primary', { hasText: 'Log this hibah' }).click(); + await page.waitForTimeout(200); + await page.locator('.guard-buttons button.btn-secondary', { hasText: 'No' }).click(); + await page.waitForTimeout(200); + await page.locator('button.btn-primary', { hasText: 'Confirm and save' }).click(); + await page.waitForTimeout(200); + const fastBadgeVisible = await page.locator('.fast-badge').isVisible(); + record('Hibah: linked gift shows fast-path badge', fastBadgeVisible); + + // Coverage should now show 100% + await clickTab('Coverage'); + const pctAfter = await page.locator('.big-percent').textContent(); + record('Coverage: reflects 100% after asset linked to completed Hibah', pctAfter.trim() === '100%', pctAfter); + const successVisible = await page.locator('.success-box').isVisible(); + record('Coverage: success box shown at full coverage', successVisible); + + // Wassiyah slow-path warning + await clickTab('Wassiyah'); + const slowWarningVisible = await page.locator('.slow-path-warning').isVisible(); + const slowWarningText = await page.locator('.slow-path-warning').textContent(); + record('Wassiyah: slow-path warning visible and explicit', slowWarningVisible && slowWarningText.includes('does not bypass')); + + // Nomination Registry basic flow + await clickTab('Assets'); + await page.locator('.field:has-text("Description") input').fill('EPF savings'); + await page.locator('.field:has-text("Estimated value") input').fill('200000'); + await page.locator('.form-card select').first().selectOption('Cash / Bank'); + await page.locator('button.btn-primary', { hasText: 'Add asset' }).click(); + await page.waitForTimeout(200); + + await clickTab('Nominate'); + await page.locator('select').first().selectOption({ label: /EPF savings/ }).catch(async () => { + const opts = await page.locator('select').first().locator('option').count(); + await page.locator('select').first().selectOption({ index: opts - 1 }); + }); + await page.locator('.field:has-text("Nominee name") input').fill('My Wife'); + await page.locator('.field:has-text("Institution") input').fill('KWSP'); + await page.locator('button.btn-primary', { hasText: 'Add nomination' }).click(); + await page.waitForTimeout(200); + const nominationRowVisible = await page.locator('.nomination-row', { hasText: 'My Wife' }).isVisible(); + record('Nomination: adding a nomination creates a row', nominationRowVisible); + + // Death Trigger: cannot fire without threshold + await clickTab('Trigger'); + const fireBtnDisabledInitially = await page.locator('button.btn-danger-solid').isDisabled(); + record('Death Trigger: fire button disabled with no attestor confirmations', fireBtnDisabledInitially); + + const attestorInputs = page.locator('.attestor-row input'); + await attestorInputs.nth(0).fill('Executor Ahmad'); + await page.locator('.attestor-row .confirm-btn').nth(0).click(); + await attestorInputs.nth(1).fill('Witness Fatimah'); + await page.locator('.attestor-row .confirm-btn').nth(1).click(); + await page.locator('.field:has-text("Date of death") input').fill('2026-08-13'); + await page.locator('.field:has-text("Death certificate reference") input').fill('DC-2026-00123'); + await page.waitForTimeout(200); + const fireBtnEnabled = await page.locator('button.btn-danger-solid').isEnabled(); + record('Death Trigger: fire button enables once 2 attestors confirm + cert ref present', fireBtnEnabled); + + await page.locator('button.btn-danger-solid').click(); + await page.waitForTimeout(300); + const triggeredBannerVisible = await page.locator('.triggered-banner').isVisible(); + record('Death Trigger: fires and shows triggered banner', triggeredBannerVisible); + + const packetRowVisible = await page.locator('.packet-row').first().isVisible(); + record('Death Trigger: execution packet(s) generated for covered assets', packetRowVisible); + + const [download] = await Promise.all([ + page.waitForEvent('download'), + page.locator('.packet-row .btn-small').first().click() + ]); + record('Death Trigger: packet download works', download.suggestedFilename().includes('execution-packet'), download.suggestedFilename()); + + record('No uncaught JS console errors during fast-path 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 8611529..156fa8b 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -4,6 +4,9 @@ import WassiyahGenerator from './lib/WassiyahGenerator.svelte'; import HibahTracker from './lib/HibahTracker.svelte'; import FamilyWaqfDesignator from './lib/FamilyWaqfDesignator.svelte'; + import NominationRegistry from './lib/NominationRegistry.svelte'; + import CoverageDashboard from './lib/CoverageDashboard.svelte'; + import DeathTrigger from './lib/DeathTrigger.svelte'; import DigitalClaims from './lib/horizon2/DigitalClaims.svelte'; import { exportAll, deleteAll } from './lib/storage.js'; import { lang, setLang } from './lib/i18n.js'; @@ -11,8 +14,8 @@ let currentLang = $state('en'); lang.subscribe(v => currentLang = v); - const tabs = ['Faraid', 'Assets', 'Wassiyah', 'Hibah', 'Family Waqf', 'Claims (H2)', 'Settings']; - const icons = ['πŸ“Š', 'πŸ“', 'πŸ“œ', '🎁', 'β›²', 'πŸ”—', 'βš™οΈ']; + const tabs = ['Coverage', 'Faraid', 'Assets', 'Wassiyah', 'Hibah', 'Family Waqf', 'Nominate', 'Trigger', 'Claims (H2)', 'Settings']; + const icons = ['🎯', 'πŸ“Š', 'πŸ“', 'πŸ“œ', '🎁', 'β›²', 'πŸ“‡', '⚑', 'πŸ”—', 'βš™οΈ']; let activeTab = $state(0); function handleKeydown(e) { @@ -59,13 +62,16 @@
- {#if activeTab === 0} - {:else if activeTab === 1} - {:else if activeTab === 2} - {:else if activeTab === 3} - {:else if activeTab === 4} - {:else if activeTab === 5} - {:else if activeTab === 6} + {#if activeTab === 0} + {:else if activeTab === 1} + {:else if activeTab === 2} + {:else if activeTab === 3} + {:else if activeTab === 4} + {:else if activeTab === 5} + {:else if activeTab === 6} + {:else if activeTab === 7} + {:else if activeTab === 8} + {:else if activeTab === 9}

Settings

Your data is stored locally on this device. Nothing is sent to a third party.

diff --git a/src/lib/CoverageDashboard.svelte b/src/lib/CoverageDashboard.svelte new file mode 100644 index 0000000..2dce8e1 --- /dev/null +++ b/src/lib/CoverageDashboard.svelte @@ -0,0 +1,106 @@ + + +
+

Coverage Dashboard

+

What fraction of your estate is already arranged to skip Faraid/probate entirely.

+ +
0 && coverage.fastPercent < 100} class:none={coverage.fastPercent === 0}> +
{coverage.fastPercent}%
+
of your estate bypasses the Faraid/probate queue
+
+ +
+
+ Fast path + {coverage.fastTotal.toLocaleString()} +
+
+ Exposed to Faraid/probate + {coverage.exposedTotal.toLocaleString()} +
+
+ + {#if coverage.fastPercent < 100 && coverage.total > 0} +
+ {coverage.exposedTotal.toLocaleString()} is still exposed. Anything not covered by Hibah, Waqf, or a Nomination channel goes into the conventional Faraid/probate process at death β€” the ~2-year route this app exists to avoid. Cover the remaining assets below. +
+ {:else if coverage.total > 0} +
+ Full coverage. Every logged asset has a fast-path exit. Nothing here is queued for Faraid/probate adjudication. +
+ {/if} + +
+ {#each coverage.rows as r (r.asset.id)} + {@const suggestion = !r.fast ? suggestedChannel(r.asset.type) : null} +
+
+ {r.asset.description} + {r.ownedValue.toLocaleString()} Β· {CHANNEL_LABELS[r.channel]} + {#if suggestion} + Next step: {suggestion.label} β€” go to {suggestion.channel === 'trust' ? 'a legal advisor for trust setup' : 'Nomination Registry'} or {r.asset.type?.toLowerCase().includes('property') ? 'Hibah/Waqf' : 'Hibah'}. + {/if} +
+ +
+ {:else} +

No assets logged yet β€” start in Asset Registry.

+ {/each} +
+ + +
+ + diff --git a/src/lib/DeathTrigger.svelte b/src/lib/DeathTrigger.svelte new file mode 100644 index 0000000..80196f2 --- /dev/null +++ b/src/lib/DeathTrigger.svelte @@ -0,0 +1,177 @@ + + +
+

Death Trigger & Execution

+

Set up now, fires at death. This is the mechanism that turns "arranged" into "distributed" β€” target: days, not the ~2-year Faraid/probate route.

+ + {#if !triggered} +
+

1. Executor

+ + +

2. Attestors

+

At least {threshold} confirmations plus a death certificate reference are required to fire the trigger β€” no single person can trigger this alone, and it cannot fire silently.

+ {#each attestors as a, i} +
+ save('attestors', attestors)} /> + +
+ {/each} + + +

3. Death record

+ + + +
+ {confirmedCount} / {threshold} attestor confirmations + {#if canTrigger}Ready to trigger{/if} +
+ + +
+ {:else} +
+ TRIGGERED. Death confirmed {dateOfDeath} by {confirmedCount} attestors. Execution packets below are ready for each covered asset. +
+ +
+ {coverage.fastPercent}% of the estate has an execution packet below. + {#if coverage.exposedTotal > 0} + {coverage.exposedTotal.toLocaleString()} has no fast-path coverage and must go through the conventional Faraid/probate process β€” this was the gap the Coverage Dashboard flagged before death. + {/if} +
+ + {#each coverage.rows.filter(r => r.fast) as r (r.asset.id)} +
+
+ {r.asset.description} + {r.channel} Β· {r.ownedValue.toLocaleString()} +
+ +
+ {/each} + + + {/if} +
+ + diff --git a/src/lib/FamilyWaqfDesignator.svelte b/src/lib/FamilyWaqfDesignator.svelte index 07389e4..4c75a71 100644 --- a/src/lib/FamilyWaqfDesignator.svelte +++ b/src/lib/FamilyWaqfDesignator.svelte @@ -12,7 +12,9 @@ const assets = load('assets', []); const total = estateTotal(assets); - let corpusAssetId = $state(''); + let corpusAssetId = $state(load('waqfCorpusAssetId', '')); + + $effect(() => { save('waqfCorpusAssetId', corpusAssetId); }); let mutawalli = $state(''); let successorMutawalli = $state(''); let beneficiaries = $state(load('waqfBeneficiaries', [])); @@ -74,7 +76,8 @@
Open fiqh question: whether a healthy-state family waqf is subject to a one-third cap has not received scholarly sign-off (tracked as OPEN-01). This module ships without waiting on that review, per explicit product direction β€” the exported deed says so plainly rather than presenting invented certainty.
-

Dedicate a specific asset to your family in perpetuity. Framed as "structure your estate proactively" β€” never as a faraid-avoidance feature.

+
This is a fast-path instrument. Once dedicated, this asset is no longer part of your estate β€” it is not subject to Faraid/probate adjudication at death. Product direction for this build is explicit: keeping assets out of the Faraid/probate queue entirely is the point.
+

Dedicate a specific asset to your family in perpetuity.

+ @@ -79,6 +86,7 @@

{g.description}

{g.date} {#if g.flagged}Flagged β€” terminal-illness context{/if} + {#if g.linkedAssetId}Fast-path: out of probate estate{/if}
@@ -93,6 +101,8 @@ diff --git a/src/lib/WassiyahGenerator.svelte b/src/lib/WassiyahGenerator.svelte index df2a17a..9c8362c 100644 --- a/src/lib/WassiyahGenerator.svelte +++ b/src/lib/WassiyahGenerator.svelte @@ -68,6 +68,9 @@

Wassiyah Generator

+
+ This does not bypass Faraid/probate. A wassiyah is a post-death instrument β€” the assets it covers are still part of your estate at death and still go through the conventional Faraid/probate process to be executed. If your goal is distribution within 1-2 weeks of death, use Hibah (lifetime gift) or Waqf for those assets instead β€” a wassiyah is only appropriate for the discretionary one-third you specifically want administered through the will process (e.g. bequests to non-heirs), not as a fast-track vehicle. +

Discretionary bequest limited to one-third of your net estate, calculated against your Asset Registry.

@@ -118,6 +121,7 @@