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
+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>