Rebuild around the real mechanism: bypass Faraid/probate entirely

Product direction clarified: Faraid/probate court adjudication is what takes
~2 years. Assets already moved out of the estate before death (via completed
Hibah, dedicated Waqf, or a legally-direct nomination channel) are never in
that queue — there's nothing for a court to adjudicate. The app's job is to
maximize what's covered by those instruments and make the death-triggered
payout fast for whatever is covered.

New:
- nonprobate.js: coverage model — classifies each Asset Registry entry by
  fast-path channel (hibah/waqf/nomination) vs exposed (faraid/probate).
- CoverageDashboard.svelte: the single number that matters — % of estate
  that bypasses Faraid entirely, with per-asset next-step suggestions.
- NominationRegistry.svelte: third fast-path channel for assets that can't
  be fully gifted/waqf'd — EPF (EPF Act 1991 s.51), Takaful/insurance
  (Insurance Act 1996 s.166), bank death-mandate, trust/nominee holding.
- DeathTrigger.svelte: the execution layer. Multi-attestor threshold +
  death certificate reference fires the trigger; generates a ready-to-file
  execution packet per covered asset, pre-filled from Hibah/Waqf/Nomination
  records. Explicitly scoped: this app cannot itself move money or transfer
  title, but removes missing-paperwork/ambiguity as a source of delay so
  institutions can act in days instead of stacking behind a probate queue.

Changed:
- HibahTracker/FamilyWaqfDesignator: now link to a specific Asset Registry
  entry so coverage can be computed; fast-path explanation added to both.
- WassiyahGenerator: explicit warning that a wassiyah does NOT bypass
  probate — it's a post-death instrument, only appropriate for the
  discretionary one-third, not a fast-track vehicle. Users were previously
  not told this distinction.
- FaraidCalculator: reframed as informational-only, showing what applies to
  whatever remains uncovered — not itself a mechanism.

e2e-fastpath.cjs: new Playwright suite covering the full mechanism
end-to-end (coverage 0%->100%, nomination, multi-attestor trigger threshold,
execution packet generation) — 16/16 passing. Original e2e-uat.cjs suite
re-verified at 32/32 with no regressions.
This commit is contained in:
wmj
2026-08-13 17:21:44 +08:00
parent 3badc5caaa
commit ad2b3c48a3
10 changed files with 625 additions and 13 deletions
+127
View File
@@ -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); });