Files
nur-falah-prevention/src/lib/calc/zakat.js
T
wmj 97a1d2ebe2 feat: close final competitive gap — professional review workflow + multi-country (MY/SG/UK)
Closes the 4th and final gap from the competitive benchmark: the
human-in-the-loop professional tier every commercial competitor pairs
with their software. Deliberately NOT a marketplace or payment
integration — a structured review-request status on each member's
Wassiyah (not_requested -> requested -> reviewed, with reviewer name
recorded), surfaced on the Mutawalli dashboard so nothing quietly ships
as final without the legally-required review being flagged.

Paired with a new whole-app jurisdiction setting since 'necessary in
certain countries' only makes sense per-jurisdiction:

- nf_families.jurisdiction (MY/SG/UK), owner-only to change — first
  UPDATE policy ever added to nf_families, which previously had none.
- New Settings-tab jurisdiction selector (JurisdictionSetting.svelte).
- Wassiyah tab and the draft will document now carry jurisdiction-
  specific legal notes: Wills Act 1959 + state Syariah (Malaysia),
  Wills Act 1838 + AMLA (Singapore), Wills Act 1837 (UK). Singapore
  added as a full third jurisdiction option, not just a stub.
- Zakat calculator's currency label and Nisab default now follow the
  family's jurisdiction (RM/SGD/GBP) instead of a hardcoded RM guess.

Covered by e2e-jurisdiction-review.cjs (13/13). Full regression:
253/253 across all suites (2 reruns confirmed as pre-existing
parallel-load flakes, not regressions).
2026-08-14 12:37:25 +08:00

32 lines
1.5 KiB
JavaScript

// Shared Zakat calculation core — Nisab check + 2.5% (1/40) on qualifying
// wealth held above threshold. Nisab is entered as a value, not a live gold
// price feed — matches the app's "manual entry only" philosophy already
// established for Asset Registry / Faraid.
const ZAKAT_RATE = 0.025;
// Rough starting points, NOT live prices — the app has no gold/silver price
// feed (manual entry only, matching the rest of the app). These exist so a
// new user isn't staring at a blank/zero Nisab field; they must still verify
// today's actual 85g-gold-equivalent value before relying on the result.
export const CURRENCY_BY_JURISDICTION = { MY: 'RM', SG: 'SGD', UK: 'GBP' };
export const DEFAULT_NISAB_BY_JURISDICTION = { MY: 24000, SG: 8000, UK: 5000 };
export function zakatableWealth(fields) {
const gross = (Number(fields.cash) || 0) + (Number(fields.gold) || 0) + (Number(fields.silver) || 0)
+ (Number(fields.businessAssets) || 0) + (Number(fields.investments) || 0) + (Number(fields.otherZakatable) || 0);
const deductible = Number(fields.deductibleLiabilities) || 0;
return Math.max(0, gross - deductible);
}
export function zakatDue(fields) {
const wealth = zakatableWealth(fields);
const nisab = Number(fields.nisabThreshold) || 0;
if (nisab <= 0 || wealth < nisab) return 0;
return wealth * ZAKAT_RATE;
}
export function meetsNisab(fields) {
const nisab = Number(fields.nisabThreshold) || 0;
return nisab > 0 && zakatableWealth(fields) >= nisab;
}