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