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:
@@ -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>
|
||||
Reference in New Issue
Block a user