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,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 & 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>
|
||||
Reference in New Issue
Block a user