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); });
+15 -9
View File
@@ -4,6 +4,9 @@
import WassiyahGenerator from './lib/WassiyahGenerator.svelte'; import WassiyahGenerator from './lib/WassiyahGenerator.svelte';
import HibahTracker from './lib/HibahTracker.svelte'; import HibahTracker from './lib/HibahTracker.svelte';
import FamilyWaqfDesignator from './lib/FamilyWaqfDesignator.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 DigitalClaims from './lib/horizon2/DigitalClaims.svelte';
import { exportAll, deleteAll } from './lib/storage.js'; import { exportAll, deleteAll } from './lib/storage.js';
import { lang, setLang } from './lib/i18n.js'; import { lang, setLang } from './lib/i18n.js';
@@ -11,8 +14,8 @@
let currentLang = $state('en'); let currentLang = $state('en');
lang.subscribe(v => currentLang = v); lang.subscribe(v => currentLang = v);
const tabs = ['Faraid', 'Assets', 'Wassiyah', 'Hibah', 'Family Waqf', 'Claims (H2)', 'Settings']; const tabs = ['Coverage', 'Faraid', 'Assets', 'Wassiyah', 'Hibah', 'Family Waqf', 'Nominate', 'Trigger', 'Claims (H2)', 'Settings'];
const icons = ['📊', '📁', '📜', '🎁', '⛲', '🔗', '⚙️']; const icons = ['🎯', '📊', '📁', '📜', '🎁', '⛲', '📇', '⚡', '🔗', '⚙️'];
let activeTab = $state(0); let activeTab = $state(0);
function handleKeydown(e) { function handleKeydown(e) {
@@ -59,13 +62,16 @@
</nav> </nav>
<main> <main>
{#if activeTab === 0}<FaraidCalculator /> {#if activeTab === 0}<CoverageDashboard />
{:else if activeTab === 1}<AssetRegistry /> {:else if activeTab === 1}<FaraidCalculator />
{:else if activeTab === 2}<WassiyahGenerator /> {:else if activeTab === 2}<AssetRegistry />
{:else if activeTab === 3}<HibahTracker /> {:else if activeTab === 3}<WassiyahGenerator />
{:else if activeTab === 4}<FamilyWaqfDesignator /> {:else if activeTab === 4}<HibahTracker />
{:else if activeTab === 5}<DigitalClaims /> {:else if activeTab === 5}<FamilyWaqfDesignator />
{:else if activeTab === 6} {:else if activeTab === 6}<NominationRegistry />
{:else if activeTab === 7}<DeathTrigger />
{:else if activeTab === 8}<DigitalClaims />
{:else if activeTab === 9}
<div class="module"> <div class="module">
<h2>Settings</h2> <h2>Settings</h2>
<p class="sub">Your data is stored locally on this device. Nothing is sent to a third party.</p> <p class="sub">Your data is stored locally on this device. Nothing is sent to a third party.</p>
+106
View File
@@ -0,0 +1,106 @@
<script>
// The whole point of this app, made visible as one number: what fraction of the
// estate is already arranged to bypass Faraid/probate entirely, versus what will
// still fall into the slow court queue because no fast-path instrument covers it.
import { computeCoverage, suggestedChannel } from './nonprobate.js';
import Disclaimer from './Disclaimer.svelte';
const coverage = computeCoverage();
const CHANNEL_LABELS = {
hibah: 'Hibah — lifetime gift, completed',
waqf: 'Waqf — dedicated corpus',
epf: 'EPF nomination',
takaful: 'Takaful / insurance nomination',
'bank-mandate': 'Bank death mandate',
trust: 'Trust / nominee holding',
'hibah-flagged-capped': 'Hibah flagged (marad al-mawt) — capped, partially exposed',
none: 'Not covered — exposed to Faraid/probate'
};
</script>
<div class="module">
<h2>Coverage Dashboard</h2>
<p class="sub">What fraction of your estate is already arranged to skip Faraid/probate entirely.</p>
<div class="big-number-card" class:full={coverage.fastPercent === 100} class:partial={coverage.fastPercent > 0 && coverage.fastPercent < 100} class:none={coverage.fastPercent === 0}>
<div class="big-percent">{coverage.fastPercent}%</div>
<div class="big-label">of your estate bypasses the Faraid/probate queue</div>
</div>
<div class="split-row">
<div class="split-card fast">
<span class="split-label">Fast path</span>
<strong>{coverage.fastTotal.toLocaleString()}</strong>
</div>
<div class="split-card slow">
<span class="split-label">Exposed to Faraid/probate</span>
<strong>{coverage.exposedTotal.toLocaleString()}</strong>
</div>
</div>
{#if coverage.fastPercent < 100 && coverage.total > 0}
<div class="warning-box">
<strong>{coverage.exposedTotal.toLocaleString()} is still exposed.</strong> 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.
</div>
{:else if coverage.total > 0}
<div class="success-box">
<strong>Full coverage.</strong> Every logged asset has a fast-path exit. Nothing here is queued for Faraid/probate adjudication.
</div>
{/if}
<div class="asset-list">
{#each coverage.rows as r (r.asset.id)}
{@const suggestion = !r.fast ? suggestedChannel(r.asset.type) : null}
<div class="asset-row" class:fast={r.fast} class:slow={!r.fast}>
<div class="asset-info">
<strong>{r.asset.description}</strong>
<span class="muted">{r.ownedValue.toLocaleString()} · {CHANNEL_LABELS[r.channel]}</span>
{#if suggestion}
<span class="suggestion-inline">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'}.</span>
{/if}
</div>
<span class="status-dot" class:on={r.fast}></span>
</div>
{:else}
<p class="empty">No assets logged yet — start in Asset Registry.</p>
{/each}
</div>
<Disclaimer text="Coverage shown here reflects what you've logged in this app, not a legal audit. EPF/Takaful nominations, bank mandates, and trust structures only take effect once actually filed with the institution — this dashboard tracks intent, not confirmed legal status." />
</div>
<style>
.module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 18px; }
.big-number-card { text-align: center; border-radius: 16px; padding: 24px; margin-bottom: 16px; border: 1px solid rgba(255,255,255,0.08); }
.big-number-card.full { background: rgba(46,204,113,0.1); border-color: rgba(46,204,113,0.3); }
.big-number-card.partial { background: rgba(201,168,76,0.08); border-color: rgba(201,168,76,0.3); }
.big-number-card.none { background: rgba(239,68,68,0.08); border-color: rgba(239,68,68,0.3); }
.big-percent { font-family: 'DM Serif Display', serif; font-size: 48px; color: #E8E4DC; }
.big-number-card.full .big-percent { color: #2ECC71; }
.big-number-card.none .big-percent { color: #EF4444; }
.big-label { font-size: 12px; color: #B8B2A6; margin-top: 4px; }
.split-row { display: flex; gap: 10px; margin-bottom: 16px; }
.split-card { flex: 1; background: rgba(255,255,255,0.03); border-radius: 10px; padding: 12px; }
.split-card.fast { border-left: 3px solid #2ECC71; }
.split-card.slow { border-left: 3px solid #EF4444; }
.split-label { display: block; font-size: 11px; color: #8A8478; margin-bottom: 4px; }
.split-card strong { font-size: 15px; color: #E8E4DC; }
.warning-box { background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.25); border-radius: 10px; padding: 12px; font-size: 12px; color: #E8E4DC; line-height: 1.5; margin-bottom: 16px; }
.success-box { background: rgba(46,204,113,0.08); border: 1px solid rgba(46,204,113,0.25); border-radius: 10px; padding: 12px; font-size: 12px; color: #E8E4DC; line-height: 1.5; margin-bottom: 16px; }
.asset-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 12px; border-radius: 10px; margin-bottom: 8px; }
.asset-row.fast { background: rgba(46,204,113,0.05); }
.asset-row.slow { background: rgba(239,68,68,0.05); }
.asset-info { display: flex; flex-direction: column; gap: 3px; font-size: 13px; color: #E8E4DC; }
.muted { color: #8A8478; font-size: 11px; }
.suggestion-inline { color: #C9A84C; font-size: 11px; }
.status-dot { width: 10px; height: 10px; border-radius: 50%; background: #EF4444; margin-top: 4px; flex-shrink: 0; }
.status-dot.on { background: #2ECC71; }
.empty { font-size: 13px; color: #8A8478; text-align: center; padding: 20px 0; }
</style>
+177
View File
@@ -0,0 +1,177 @@
<script>
// The execution layer. Everything else in this app exists to get assets OUT of
// the probate-bound estate before death. This module is what fires WHEN death
// happens: verify it via multiple attestors (not a single point of failure or
// fraud), then generate ready-to-file execution packets for every fast-path
// instrument already on record — so a family is filing paperwork in days, not
// waiting on a Shariah Court docket for years.
//
// What this can and cannot do, stated plainly:
// - CAN: generate the correct claim/transfer paperwork for each covered asset,
// pre-filled from the Asset Registry, Hibah, Waqf, and Nomination records.
// - CANNOT: itself move money, transfer land title, or force an institution to
// act — EPF, the bank, Takaful, and the Land Office are the ones who actually
// execute, on their own timelines. This app's job is to remove every reason
// for THEM to be slow (no missing paperwork, no ambiguity about who gets what,
// no court step required for covered assets) so their part can be done in the
// 1-2 week window instead of stacking behind a 2-year probate queue.
import { load, save } from './storage.js';
import { computeCoverage } from './nonprobate.js';
const coverage = computeCoverage();
let executorName = $state(load('executorName', ''));
let attestors = $state(load('attestors', [{ name: '', confirmed: false }, { name: '', confirmed: false }]));
let deathCertRef = $state('');
let dateOfDeath = $state('');
let triggered = $state(load('deathTriggered', false));
function saveExecutor() {
save('executorName', executorName);
}
function addAttestor() {
attestors = [...attestors, { name: '', confirmed: false }];
}
function toggleConfirm(i) {
attestors[i].confirmed = !attestors[i].confirmed;
attestors = [...attestors];
save('attestors', attestors);
}
const confirmedCount = $derived(attestors.filter(a => a.confirmed && a.name).length);
const threshold = 2; // executor + at least 1 more attestor, or 2 named attestors — simple majority-of-small-group demo
const canTrigger = $derived(confirmedCount >= threshold && deathCertRef && dateOfDeath);
function fireTrigger() {
if (!canTrigger) return;
triggered = true;
save('deathTriggered', true);
save('deathCertRef', deathCertRef);
save('dateOfDeath', dateOfDeath);
}
function resetDemo() {
triggered = false;
save('deathTriggered', false);
}
function downloadPacket(row) {
const lines = [
'EXECUTION PACKET — NON-PROBATE ASSET TRANSFER',
`Asset: ${row.asset.description} (${row.asset.type})`,
`Channel: ${row.channel}`,
`Value: ${row.ownedValue.toLocaleString()}`,
`Date of death: ${dateOfDeath}`,
`Death certificate reference: ${deathCertRef}`,
`Attestors confirming: ${attestors.filter(a => a.confirmed && a.name).map(a => a.name).join(', ')}`,
'',
row.channel === 'hibah'
? 'ACTION: This asset was already gifted (Hibah) with completed offer, acceptance,\nand possession prior to death. It is not part of the estate. No probate filing is\nrequired for this asset. Attach the Hibah record and recipient acknowledgment as\nproof of prior transfer if the institution requests confirmation.'
: row.channel === 'waqf'
? 'ACTION: This asset was already dedicated as Waqf prior to death. It is not part\nof the estate. File the waqfiyya deed with the relevant state waqf authority for\nadministrative continuity — this is a registration step, not a probate step.'
: row.channel === 'epf'
? 'ACTION: File an EPF death claim (Borang KWSP 9K or current equivalent) with the\ndeath certificate. EPF pays the nominee directly under EPF Act 1991 s.51 —\nno Letters of Administration required.'
: row.channel === 'takaful'
? 'ACTION: File a death claim with the Takaful/insurance provider. As a nominated\ntrust under Insurance Act 1996 s.166 (or Takaful equivalent), payout goes directly\nto the named beneficiary — no probate required.'
: row.channel === 'bank-mandate'
? 'ACTION: Notify the bank with the death certificate and the death-mandate /\njoint-account documentation on file. Funds release per the mandate terms — this\nis a bank process, not a court process.'
: 'ACTION: Contact the trustee/nominee holder to execute the pre-arranged transfer\nunder the trust deed. This is an internal trustee record update, not a probate\nfiling.',
'',
'This packet was generated by Nur Falah on a local device. It is a drafting aid;',
'the receiving institution\'s own claim process still applies. Its purpose is to',
'remove ambiguity and missing paperwork as a source of delay.'
];
const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `execution-packet-${row.asset.description.replace(/\s+/g, '-').toLowerCase()}.txt`; a.click();
URL.revokeObjectURL(url);
}
</script>
<div class="module">
<h2>Death Trigger &amp; Execution</h2>
<p class="sub">Set up now, fires at death. This is the mechanism that turns "arranged" into "distributed" — target: days, not the ~2-year Faraid/probate route.</p>
{#if !triggered}
<div class="setup-card">
<h3>1. Executor</h3>
<label class="field"><span>Executor name</span><input type="text" bind:value={executorName} oninput={saveExecutor} /></label>
<h3>2. Attestors</h3>
<p class="note">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.</p>
{#each attestors as a, i}
<div class="attestor-row">
<input type="text" placeholder="Attestor name" bind:value={a.name} oninput={() => save('attestors', attestors)} />
<button class="confirm-btn" class:on={a.confirmed} onclick={() => toggleConfirm(i)}>{a.confirmed ? 'Confirmed' : 'Confirm death'}</button>
</div>
{/each}
<button class="btn-secondary" onclick={addAttestor}>Add another attestor</button>
<h3>3. Death record</h3>
<label class="field"><span>Date of death</span><input type="date" bind:value={dateOfDeath} /></label>
<label class="field"><span>Death certificate reference number</span><input type="text" bind:value={deathCertRef} /></label>
<div class="trigger-status">
<span>{confirmedCount} / {threshold} attestor confirmations</span>
{#if canTrigger}<span class="ready">Ready to trigger</span>{/if}
</div>
<button class="btn-danger-solid" disabled={!canTrigger} onclick={fireTrigger}>Fire death trigger — begin execution</button>
</div>
{:else}
<div class="triggered-banner">
<strong>TRIGGERED.</strong> Death confirmed {dateOfDeath} by {confirmedCount} attestors. Execution packets below are ready for each covered asset.
</div>
<div class="coverage-summary">
<span>{coverage.fastPercent}% of the estate has an execution packet below.</span>
{#if coverage.exposedTotal > 0}
<span class="exposed-note">{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.</span>
{/if}
</div>
{#each coverage.rows.filter(r => r.fast) as r (r.asset.id)}
<div class="packet-row">
<div>
<strong>{r.asset.description}</strong>
<span class="muted">{r.channel} · {r.ownedValue.toLocaleString()}</span>
</div>
<button class="btn-small" onclick={() => downloadPacket(r)}>Download packet</button>
</div>
{/each}
<button class="btn-secondary" onclick={resetDemo}>Reset (testing only)</button>
{/if}
</div>
<style>
.module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 18px; line-height: 1.5; }
.setup-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 16px; }
.setup-card h3 { font-size: 14px; color: #C9A84C; margin: 16px 0 8px; }
.setup-card h3:first-child { margin-top: 0; }
.note { font-size: 11.5px; color: #8A8478; margin-bottom: 10px; line-height: 1.5; }
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.field span { font-size: 12px; color: #B8B2A6; }
.field input { background: rgba(255,255,255,0.05); border: 1px solid rgba(201,168,76,0.2); border-radius: 8px; padding: 10px 12px; color: #E8E4DC; font-size: 14px; width: 100%; }
.attestor-row { display: flex; gap: 8px; margin-bottom: 8px; }
.attestor-row input { flex: 1; background: rgba(255,255,255,0.05); border: 1px solid rgba(201,168,76,0.2); border-radius: 8px; padding: 8px 10px; color: #E8E4DC; font-size: 13px; }
.confirm-btn { padding: 8px 12px; border-radius: 8px; border: 1px solid rgba(239,68,68,0.3); background: rgba(239,68,68,0.1); color: #EF4444; font-size: 11px; cursor: pointer; white-space: nowrap; }
.confirm-btn.on { border-color: rgba(46,204,113,0.4); background: rgba(46,204,113,0.15); color: #2ECC71; }
.btn-secondary { width: 100%; padding: 10px; border-radius: 8px; border: none; background: rgba(255,255,255,0.08); color: #E8E4DC; font-weight: 600; cursor: pointer; margin: 8px 0 4px; font-size: 13px; }
.trigger-status { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #B8B2A6; margin: 16px 0 10px; }
.ready { color: #2ECC71; font-weight: 600; }
.btn-danger-solid { width: 100%; padding: 14px; border-radius: 8px; border: none; background: #EF4444; color: white; font-weight: 700; cursor: pointer; }
.btn-danger-solid:disabled { opacity: 0.3; cursor: not-allowed; }
.triggered-banner { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 10px; padding: 14px; font-size: 13px; color: #E8E4DC; margin-bottom: 14px; line-height: 1.5; }
.coverage-summary { background: rgba(255,255,255,0.03); border-radius: 10px; padding: 12px; margin-bottom: 14px; display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: #B8B2A6; }
.exposed-note { color: #EF4444; }
.packet-row { display: flex; justify-content: space-between; align-items: center; padding: 12px; background: rgba(46,204,113,0.05); border-radius: 10px; margin-bottom: 8px; font-size: 13px; color: #E8E4DC; }
.muted { display: block; color: #8A8478; font-size: 11px; margin-top: 2px; }
.btn-small { background: rgba(46,204,113,0.15); color: #2ECC71; border: none; border-radius: 8px; padding: 8px 12px; font-size: 12px; cursor: pointer; font-weight: 600; }
</style>
+6 -2
View File
@@ -12,7 +12,9 @@
const assets = load('assets', []); const assets = load('assets', []);
const total = estateTotal(assets); const total = estateTotal(assets);
let corpusAssetId = $state(''); let corpusAssetId = $state(load('waqfCorpusAssetId', ''));
$effect(() => { save('waqfCorpusAssetId', corpusAssetId); });
let mutawalli = $state(''); let mutawalli = $state('');
let successorMutawalli = $state(''); let successorMutawalli = $state('');
let beneficiaries = $state(load('waqfBeneficiaries', [])); let beneficiaries = $state(load('waqfBeneficiaries', []));
@@ -74,7 +76,8 @@
<div class="open-note"> <div class="open-note">
<strong>Open fiqh question:</strong> 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. <strong>Open fiqh question:</strong> 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.
</div> </div>
<p class="sub">Dedicate a specific asset to your family in perpetuity. Framed as "structure your estate proactively" — never as a faraid-avoidance feature.</p> <div class="fast-path-note">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.</div>
<p class="sub">Dedicate a specific asset to your family in perpetuity.</p>
<label class="field"><span>Corpus asset</span> <label class="field"><span>Corpus asset</span>
<select bind:value={corpusAssetId}> <select bind:value={corpusAssetId}>
@@ -129,6 +132,7 @@
.module { padding: 4px 0 40px; } .module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 10px; } h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 10px; }
.open-note { font-size: 11.5px; color: #C9A84C; background: rgba(201,168,76,0.08); border: 1px solid rgba(201,168,76,0.25); border-radius: 10px; padding: 10px 12px; margin-bottom: 12px; line-height: 1.5; } .open-note { font-size: 11.5px; color: #C9A84C; background: rgba(201,168,76,0.08); border: 1px solid rgba(201,168,76,0.25); border-radius: 10px; padding: 10px 12px; margin-bottom: 12px; line-height: 1.5; }
.fast-path-note { font-size: 11.5px; color: #2ECC71; background: rgba(46,204,113,0.08); border: 1px solid rgba(46,204,113,0.25); border-radius: 10px; padding: 10px 12px; margin-bottom: 12px; line-height: 1.5; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; } .sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; }
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; } .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.field span { font-size: 12px; color: #B8B2A6; } .field span { font-size: 12px; color: #B8B2A6; }
+2
View File
@@ -29,6 +29,7 @@
<div class="module"> <div class="module">
<h2>Faraid Calculator</h2> <h2>Faraid Calculator</h2>
<div class="reframe-note">This calculator is informational — it shows what would apply to whatever is <em>left</em> in your estate. It is not itself a fast-path mechanism: anything still in your estate at death goes through the conventional Faraid/probate process. Check the Coverage Dashboard and use Hibah/Waqf/Nomination to move assets out of this calculation entirely.</div>
<p class="sub">Answer simple questions about surviving family — no fiqh terminology needed.</p> <p class="sub">Answer simple questions about surviving family — no fiqh terminology needed.</p>
<label class="field"> <label class="field">
@@ -96,6 +97,7 @@
<style> <style>
.module { padding: 4px 0 40px; } .module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; } h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; }
.reframe-note { font-size: 11.5px; color: #B8B2A6; background: rgba(255,255,255,0.04); border-radius: 10px; padding: 10px 12px; margin: 10px 0 12px; line-height: 1.5; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 20px; } .sub { font-size: 13px; color: #8A8478; margin-bottom: 20px; }
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; } .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.field span { font-size: 12.5px; color: #B8B2A6; } .field span { font-size: 12.5px; color: #B8B2A6; }
+12 -2
View File
@@ -7,7 +7,7 @@
const total = estateTotal(assets); const total = estateTotal(assets);
let gifts = $state(load('hibahGifts', [])); let gifts = $state(load('hibahGifts', []));
let form = $state({ recipient: '', relation: '', description: '', date: new Date().toISOString().slice(0, 10), statement: '' }); let form = $state({ recipient: '', relation: '', description: '', date: new Date().toISOString().slice(0, 10), statement: '', linkedAssetId: '' });
let guardResult = $state(null); let guardResult = $state(null);
let showGuard = $state(false); let showGuard = $state(false);
let pendingSave = $state(false); let pendingSave = $state(false);
@@ -33,7 +33,7 @@
}; };
gifts = [...gifts, gift]; gifts = [...gifts, gift];
save('hibahGifts', gifts); save('hibahGifts', gifts);
form = { recipient: '', relation: '', description: '', date: new Date().toISOString().slice(0, 10), statement: '' }; form = { recipient: '', relation: '', description: '', date: new Date().toISOString().slice(0, 10), statement: '', linkedAssetId: '' };
showGuard = false; showGuard = false;
guardResult = null; guardResult = null;
} }
@@ -48,12 +48,19 @@
<div class="module"> <div class="module">
<h2>Hibah Tracker</h2> <h2>Hibah Tracker</h2>
<div class="fast-path-note">This is a fast-path instrument. Once offer, acceptance, and possession are complete, this asset belongs to the recipient now — it is no longer part of your estate, so there is nothing for a Faraid/probate court to adjudicate on it at death.</div>
<p class="sub">Log a lifetime gift — completed by offer, acceptance, and possession.</p> <p class="sub">Log a lifetime gift — completed by offer, acceptance, and possession.</p>
<div class="form-card"> <div class="form-card">
<label class="field"><span>Recipient</span><input type="text" bind:value={form.recipient} /></label> <label class="field"><span>Recipient</span><input type="text" bind:value={form.recipient} /></label>
<label class="field"><span>Relation to you</span><input type="text" bind:value={form.relation} /></label> <label class="field"><span>Relation to you</span><input type="text" bind:value={form.relation} /></label>
<label class="field"><span>Asset / gift description</span><input type="text" bind:value={form.description} /></label> <label class="field"><span>Asset / gift description</span><input type="text" bind:value={form.description} /></label>
<label class="field"><span>Link to a registered asset (moves it out of the probate-bound estate)</span>
<select bind:value={form.linkedAssetId}>
<option value="">Not linked — standalone gift</option>
{#each assets as a}<option value={a.id}>{a.type} {a.description}</option>{/each}
</select>
</label>
<label class="field"><span>Date</span><input type="date" bind:value={form.date} /></label> <label class="field"><span>Date</span><input type="date" bind:value={form.date} /></label>
<label class="field"><span>Offer-and-acceptance statement</span><textarea bind:value={form.statement} rows="2"></textarea></label> <label class="field"><span>Offer-and-acceptance statement</span><textarea bind:value={form.statement} rows="2"></textarea></label>
@@ -79,6 +86,7 @@
<p class="gift-desc">{g.description}</p> <p class="gift-desc">{g.description}</p>
<span class="muted">{g.date}</span> <span class="muted">{g.date}</span>
{#if g.flagged}<span class="flag-badge">Flagged — terminal-illness context</span>{/if} {#if g.flagged}<span class="flag-badge">Flagged — terminal-illness context</span>{/if}
{#if g.linkedAssetId}<span class="fast-badge">Fast-path: out of probate estate</span>{/if}
</div> </div>
<button onclick={() => remove(g.id)}>✕</button> <button onclick={() => remove(g.id)}>✕</button>
</div> </div>
@@ -93,6 +101,8 @@
<style> <style>
.module { padding: 4px 0 40px; } .module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; } h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; }
.fast-path-note { font-size: 11.5px; color: #2ECC71; background: rgba(46,204,113,0.08); border: 1px solid rgba(46,204,113,0.25); border-radius: 10px; padding: 10px 12px; margin-bottom: 12px; line-height: 1.5; }
.fast-badge { display: inline-block; margin-top: 4px; margin-left: 6px; font-size: 10.5px; background: #2ECC71; color: #070A0D; padding: 2px 8px; border-radius: 6px; font-weight: 600; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; } .sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; }
.form-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 16px; } .form-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 16px; }
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; } .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
+106
View File
@@ -0,0 +1,106 @@
<script>
// Third fast-path channel alongside Hibah and Waqf. Some assets (EPF, Takaful/
// insurance, bank accounts, land) have no practical way to be fully gifted or
// waqf'd away during life — but several of them have their own legally-direct
// nomination mechanism that already bypasses probate BY LAW, independent of
// Faraid: EPF nomination (EPF Act 1991 s.51 pays the nominee directly), Takaful/
// insurance nomination as trust (Insurance Act 1996 s.166 / Takaful equivalent),
// bank death-mandate or joint-tenancy-with-survivorship. Land and business
// interests generally have no such channel and need a pre-funded trust/nominee
// holding structure instead — a real legal step this app can prepare paperwork
// for but cannot execute on its own.
import { load, save } from './storage.js';
import { suggestedChannel } from './nonprobate.js';
import Disclaimer from './Disclaimer.svelte';
const assets = load('assets', []);
const CHANNEL_TYPES = [
{ value: 'epf', label: 'EPF nomination', note: 'Pays the nominee directly under EPF Act 1991 s.51 — bypasses probate by law.' },
{ value: 'takaful', label: 'Takaful / insurance nomination', note: 'Nomination as trust under Insurance Act 1996 s.166 (or Takaful equivalent) — pays the named beneficiary directly.' },
{ value: 'bank-mandate', label: 'Bank death mandate / joint account', note: 'Ask the bank for a death-benefit nomination, or hold as joint account with survivorship.' },
{ value: 'trust', label: 'Pre-funded trust / nominee holding', note: 'For land or illiquid assets with no nomination bypass — requires setting up a real trust or nominee entity now, while alive.' }
];
let nominations = $state(load('nominations', []));
let form = $state({ linkedAssetId: '', type: 'epf', institution: '', nomineeeName: '', referenceNumber: '' });
function addNomination() {
if (!form.linkedAssetId || !form.nomineeeName) return;
nominations = [...nominations, { ...form, id: crypto.randomUUID() }];
save('nominations', nominations);
form = { linkedAssetId: '', type: 'epf', institution: '', nomineeeName: '', referenceNumber: '' };
}
function remove(id) {
nominations = nominations.filter(n => n.id !== id);
save('nominations', nominations);
}
const selectedAsset = $derived(assets.find(a => a.id === form.linkedAssetId));
const suggestion = $derived(selectedAsset ? suggestedChannel(selectedAsset.type) : null);
</script>
<div class="module">
<h2>Nomination Registry</h2>
<div class="fast-path-note">Third fast-path channel. EPF and Takaful/insurance nominations pay the nominee directly by law — no Faraid/probate involvement at all. Bank mandates and trust/nominee holdings need setup now, while alive, but also bypass the court queue once in place.</div>
<p class="sub">Log which non-probate channel covers each asset that can't be fully gifted (Hibah) or dedicated (Waqf).</p>
<div class="form-card">
<label class="field"><span>Asset</span>
<select bind:value={form.linkedAssetId}>
<option value="">Select from Asset Registry…</option>
{#each assets as a}<option value={a.id}>{a.type} {a.description}</option>{/each}
</select>
</label>
{#if suggestion}
<p class="suggestion">Suggested channel for this asset type: <strong>{suggestion.label}</strong>. {suggestion.note}</p>
{/if}
<label class="field"><span>Channel type</span>
<select bind:value={form.type}>
{#each CHANNEL_TYPES as c}<option value={c.value}>{c.label}</option>{/each}
</select>
</label>
<label class="field"><span>Institution</span><input type="text" bind:value={form.institution} placeholder="e.g. KWSP, Bank Islam, Takaful Ikhlas" /></label>
<label class="field"><span>Nominee name</span><input type="text" bind:value={form.nomineeeName} /></label>
<label class="field"><span>Reference / policy / account number</span><input type="text" bind:value={form.referenceNumber} /></label>
<button class="btn-primary" onclick={addNomination}>Add nomination</button>
</div>
{#each nominations as n (n.id)}
{@const asset = assets.find(a => a.id === n.linkedAssetId)}
{@const channelInfo = CHANNEL_TYPES.find(c => c.value === n.type)}
<div class="nomination-row">
<div class="nomination-info">
<strong>{asset ? asset.description : '(asset removed)'}</strong>
<span class="muted">{channelInfo?.label} · {n.institution || '—'}</span>
<span class="muted">Nominee: {n.nomineeeName} · Ref: {n.referenceNumber || '—'}</span>
</div>
<button onclick={() => remove(n.id)}>✕</button>
</div>
{:else}
<p class="empty">No nominations logged yet.</p>
{/each}
<Disclaimer text="This registry records your nomination — it does not submit it. You must still file the actual nomination form with EPF, the Takaful/insurance provider, or the bank for it to take legal effect." />
</div>
<style>
.module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; }
.fast-path-note { font-size: 11.5px; color: #2ECC71; background: rgba(46,204,113,0.08); border: 1px solid rgba(46,204,113,0.25); border-radius: 10px; padding: 10px 12px; margin: 10px 0 12px; line-height: 1.5; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; }
.form-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 16px; }
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.field span { font-size: 12px; color: #B8B2A6; }
.field select, .field input { background: rgba(255,255,255,0.05); border: 1px solid rgba(201,168,76,0.2); border-radius: 8px; padding: 10px 12px; color: #E8E4DC; font-size: 14px; }
.suggestion { font-size: 11.5px; color: #C9A84C; background: rgba(201,168,76,0.06); border-radius: 8px; padding: 8px 10px; margin-bottom: 12px; line-height: 1.5; }
.btn-primary { width: 100%; padding: 12px; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; background: #C9A84C; color: #070A0D; }
.nomination-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 12px 0; border-bottom: 1px solid rgba(255,255,255,0.06); }
.nomination-info { display: flex; flex-direction: column; gap: 2px; font-size: 13px; color: #E8E4DC; }
.muted { color: #8A8478; font-size: 11.5px; }
.nomination-row button { background: none; border: none; color: #8A8478; cursor: pointer; }
.empty { font-size: 13px; color: #8A8478; text-align: center; padding: 20px 0; }
</style>
+4
View File
@@ -68,6 +68,9 @@
<div class="module"> <div class="module">
<h2>Wassiyah Generator</h2> <h2>Wassiyah Generator</h2>
<div class="slow-path-warning">
<strong>This does not bypass Faraid/probate.</strong> 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 <strong>Hibah</strong> (lifetime gift) or <strong>Waqf</strong> 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.
</div>
<p class="sub">Discretionary bequest limited to one-third of your net estate, calculated against your Asset Registry.</p> <p class="sub">Discretionary bequest limited to one-third of your net estate, calculated against your Asset Registry.</p>
<div class="meter-card"> <div class="meter-card">
@@ -118,6 +121,7 @@
<style> <style>
.module { padding: 4px 0 40px; } .module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; } h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; }
.slow-path-warning { font-size: 11.5px; color: #EF4444; background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.25); border-radius: 10px; padding: 10px 12px; margin: 10px 0 12px; line-height: 1.5; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; } .sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; }
.meter-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 18px; } .meter-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 18px; }
.meter-row { display: flex; justify-content: space-between; font-size: 13px; color: #B8B2A6; margin-bottom: 6px; } .meter-row { display: flex; justify-content: space-between; font-size: 13px; color: #B8B2A6; margin-bottom: 6px; }
+70
View File
@@ -0,0 +1,70 @@
// Non-probate coverage model — the actual mechanism behind the 1-2 week promise.
//
// Faraid/probate (Shariah Court determination of heirs + Letters of Administration
// + Land Office transfer) is what takes ~2 years in the conventional system. It only
// applies to whatever is STILL part of the estate at death. Assets already moved out
// of the estate before death — via completed Hibah (lifetime gift), dedicated Waqf,
// or a legally-recognised non-probate nomination/trust channel — are never in that
// queue at all; there is nothing for a court to adjudicate.
//
// So "coverage" here means: what fraction of the Asset Registry total is already
// arranged to bypass probate entirely, versus what will still fall into the slow
// Faraid/probate queue because no fast-path instrument has been set up for it.
//
// Fast-path channels modelled:
// - hibah: completed lifetime gift (HibahTracker, non-flagged or resolved)
// - waqf: dedicated corpus (FamilyWaqfDesignator)
// - nomination: legally-direct-to-nominee channel (EPF, Takaful/insurance under
// Insurance Act 1996 s.166, bank death-mandate, joint tenancy)
// - trust: pre-funded trust/nominee holding structure (for land/illiquid
// assets that have no nomination-based bypass) — requires an actual
// trust entity to exist; the app can only record that one has been
// set up, it cannot create the legal entity itself.
import { load } from './storage.js';
export function computeCoverage() {
const assets = load('assets', []);
const gifts = load('hibahGifts', []);
const nominations = load('nominations', []);
// waqf corpus assets: any asset referenced by a saved waqf corpus selection
const waqfCorpusId = load('waqfCorpusAssetId', null);
const rows = assets.map(a => {
const ownedValue = (Number(a.value) || 0) * (Number(a.ownershipShare ?? 100) / 100);
const hibahMatch = gifts.find(g => g.linkedAssetId === a.id);
const nominationMatch = nominations.find(n => n.linkedAssetId === a.id);
const isWaqfCorpus = waqfCorpusId === a.id;
let channel = 'none';
let fast = false;
if (hibahMatch && !hibahMatch.flagged) { channel = 'hibah'; fast = true; }
else if (isWaqfCorpus) { channel = 'waqf'; fast = true; }
else if (nominationMatch) { channel = nominationMatch.type; fast = true; }
else if (hibahMatch && hibahMatch.flagged) { channel = 'hibah-flagged-capped'; fast = false; }
return { asset: a, ownedValue, channel, fast };
});
const total = rows.reduce((s, r) => s + r.ownedValue, 0);
const fastTotal = rows.filter(r => r.fast).reduce((s, r) => s + r.ownedValue, 0);
const exposedTotal = total - fastTotal;
return {
rows,
total,
fastTotal,
exposedTotal,
fastPercent: total > 0 ? Math.round((fastTotal / total) * 100) : 0
};
}
/** Which non-probate channel realistically applies to an asset type, per user direction: asset-type dependent. */
export function suggestedChannel(assetType) {
const t = (assetType || '').toLowerCase();
if (t.includes('cash') || t.includes('bank')) return { channel: 'bank-mandate', label: 'Bank death mandate / joint account', note: 'Ask your bank about a death-benefit nomination or convert to a joint account with survivorship — pays out directly, no probate.' };
if (t.includes('business')) return { channel: 'trust', label: 'Pre-funded trust / nominee holding', note: 'Business interests generally need a shareholder agreement or trust structure set up now — a legal step beyond this app.' };
if (t.includes('digital')) return { channel: 'bank-mandate', label: 'Multi-sig / pre-authorized release', note: 'Crypto and digital accounts: use a multi-sig wallet or exchange beneficiary feature where available.' };
if (t.includes('property') || t.includes('vehicle') || t.includes('jewelry') || t.includes('valuables')) return { channel: 'trust', label: 'Pre-funded trust / nominee holding, or Hibah/Waqf now', note: 'Land and physical assets have no nomination-based bypass in most jurisdictions. Either complete a Hibah/Waqf now (ownership transfers immediately), or set up a trust/nominee holding structure — both require real-world legal steps this app can prepare paperwork for but cannot execute alone.' };
return { channel: 'nomination', label: 'EPF / Takaful nomination', note: 'For retirement funds and insurance/takaful, use the scheme\'s own nomination form — these pay the nominee directly by law, bypassing probate.' };
}