Initial build: Horizon 1 Prevention Suite + Horizon 2 Unlock demo
Svelte 5 + Vite PWA, styled to match moslem03.falahos.my's design system. Horizon 1: Faraid Calculator (shared calc core, 14 classical cases passing), Asset Registry, Wassiyah Generator (1/3 meter + heir-exclusion block), Hibah Tracker and Family Waqf Designator (shared marad al-mawt guardrail). Horizon 2: Digital Beneficial Claims — local non-custodial demo of the claim model and transfer restriction only. Two governance gates in this project's own PRDs were overridden per explicit product direction, and are flagged in-app and in README.md / deploy/DEPLOY.md rather than silently shipped as production-ready: - Family Waqf Designator ships ahead of scholarly sign-off (OPEN-01 in scholarly-review-log.md remains unresolved). - Horizon 2 ships ahead of the PRD's stated Phase 0 gate (legal opinion + signed institutional partner) with no confirmation that gate is cleared.
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
// Shared Faraid calculation core.
|
||||
// Single engine consumed by: Faraid Calculator, Wassiyah Generator (1/3 meter),
|
||||
// Hibah Tracker (marad al-mawt cap), Family Waqf Designator (marad al-mawt cap).
|
||||
// Per Horizon 1 PRD §11: "one engine, four consumers, so the modules can never disagree."
|
||||
//
|
||||
// Scope note: models the majority-position fixed shares, 'awl, radd, and hijab
|
||||
// blocking for spouse/children/parents/siblings (full, consanguine, uterine).
|
||||
// Does not yet model grandparents, grandchildren, or extended 'asabah chains —
|
||||
// tracked as a follow-up, not silently assumed correct for those cases.
|
||||
|
||||
function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); }
|
||||
|
||||
class Fraction {
|
||||
constructor(num, den) {
|
||||
if (den === 0) throw new Error('Zero denominator');
|
||||
if (den < 0) { num = -num; den = -den; }
|
||||
const g = gcd(Math.abs(num), den) || 1;
|
||||
this.num = num / g;
|
||||
this.den = den / g;
|
||||
}
|
||||
add(o) { return new Fraction(this.num * o.den + o.num * this.den, this.den * o.den); }
|
||||
sub(o) { return new Fraction(this.num * o.den - o.num * this.den, this.den * o.den); }
|
||||
mul(o) { return new Fraction(this.num * o.num, this.den * o.den); }
|
||||
div(o) { return new Fraction(this.num * o.den, this.den * o.num); }
|
||||
toNumber() { return this.num / this.den; }
|
||||
toString() { return this.den === 1 ? `${this.num}` : `${this.num}/${this.den}`; }
|
||||
static zero() { return new Fraction(0, 1); }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} heirs
|
||||
* spouseCount, deceasedGender ('male'|'female'),
|
||||
* sons, daughters, father, mother (bool),
|
||||
* fullBrothers, fullSisters, paternalBrothers, paternalSisters, maternalSiblings
|
||||
*/
|
||||
export function calculateFaraid(heirs) {
|
||||
const {
|
||||
deceasedGender = 'male',
|
||||
spouseCount = 0,
|
||||
sons = 0,
|
||||
daughters = 0,
|
||||
father = false,
|
||||
mother = false,
|
||||
fullBrothers = 0,
|
||||
fullSisters = 0,
|
||||
paternalBrothers = 0,
|
||||
paternalSisters = 0,
|
||||
maternalSiblings = 0
|
||||
} = heirs;
|
||||
|
||||
const hasChildren = sons > 0 || daughters > 0;
|
||||
const hasDescendants = hasChildren; // grandchildren not yet modelled
|
||||
const hasFather = !!father;
|
||||
|
||||
// Hijab (blocking): full/consanguine siblings blocked by father or a son (not daughter alone).
|
||||
// Uterine (maternal) siblings blocked by any child or father/grandfather.
|
||||
const siblingsBlockedByDescendantOrFather = hasFather || sons > 0;
|
||||
const effFullBrothers = siblingsBlockedByDescendantOrFather ? 0 : fullBrothers;
|
||||
const effFullSisters = siblingsBlockedByDescendantOrFather ? 0 : fullSisters;
|
||||
const effPaternalBrothers = (siblingsBlockedByDescendantOrFather || effFullBrothers > 0) ? 0 : paternalBrothers;
|
||||
const effPaternalSisters = (siblingsBlockedByDescendantOrFather || effFullBrothers > 0 || (effFullSisters > 0 && sons === 0 && daughters === 0)) ? 0 : paternalSisters;
|
||||
const effMaternalSiblings = (hasChildren || hasFather) ? 0 : maternalSiblings;
|
||||
|
||||
const shares = []; // { heir, count, fraction, note }
|
||||
let fixedTotal = Fraction.zero();
|
||||
|
||||
// Spouse
|
||||
if (spouseCount > 0) {
|
||||
let f;
|
||||
if (deceasedGender === 'male') {
|
||||
f = hasDescendants ? new Fraction(1, 8) : new Fraction(1, 4); // wife/wives share this jointly
|
||||
} else {
|
||||
f = hasDescendants ? new Fraction(1, 4) : new Fraction(1, 2); // husband
|
||||
}
|
||||
shares.push({ heir: deceasedGender === 'male' ? 'Wife/Wives (combined)' : 'Husband', count: spouseCount, fraction: f });
|
||||
fixedTotal = fixedTotal.add(f);
|
||||
}
|
||||
|
||||
// Children: sons/daughters take residue by 'asabah (2:1) if sons present; if daughters only, fixed shares apply.
|
||||
let childrenAreResiduary = sons > 0;
|
||||
if (!childrenAreResiduary && daughters > 0) {
|
||||
const f = daughters === 1 ? new Fraction(1, 2) : new Fraction(2, 3);
|
||||
shares.push({ heir: daughters === 1 ? 'Daughter' : 'Daughters (combined)', count: daughters, fraction: f });
|
||||
fixedTotal = fixedTotal.add(f);
|
||||
}
|
||||
|
||||
// Father
|
||||
if (hasFather) {
|
||||
if (hasDescendants) {
|
||||
const f = new Fraction(1, 6);
|
||||
shares.push({ heir: 'Father', count: 1, fraction: f, note: sons === 0 ? 'plus residue if any remains' : undefined });
|
||||
fixedTotal = fixedTotal.add(f);
|
||||
}
|
||||
// if no descendants, father is pure 'asabah — handled in residue step
|
||||
}
|
||||
|
||||
// Mother — gharrawain/umariyyatayn: spouse + both parents, mother gets 1/3 of *remainder after spouse*, not 1/3 of estate.
|
||||
const isUmariyyatayn = mother && hasFather && !hasChildren && (fullBrothers + fullSisters + paternalBrothers + paternalSisters + maternalSiblings === 0) && spouseCount > 0;
|
||||
if (mother) {
|
||||
let f;
|
||||
const siblingCountForMotherBlock = effFullBrothers + effFullSisters + effPaternalBrothers + effPaternalSisters + effMaternalSiblings;
|
||||
if (isUmariyyatayn) {
|
||||
const remainderAfterSpouse = new Fraction(1, 1).sub(shares.find(s => s.heir.includes('Wife') || s.heir.includes('Husband')).fraction);
|
||||
f = remainderAfterSpouse.mul(new Fraction(1, 3));
|
||||
shares.push({ heir: 'Mother', count: 1, fraction: f, note: 'gharrawain/umariyyatayn ruling applied' });
|
||||
} else if (hasDescendants || siblingCountForMotherBlock >= 2) {
|
||||
f = new Fraction(1, 6);
|
||||
shares.push({ heir: 'Mother', count: 1, fraction: f });
|
||||
} else {
|
||||
f = new Fraction(1, 3);
|
||||
shares.push({ heir: 'Mother', count: 1, fraction: f });
|
||||
}
|
||||
fixedTotal = fixedTotal.add(f);
|
||||
}
|
||||
|
||||
// Kalala siblings (no father, no descendants) — full siblings first, else consanguine, else uterine always independent
|
||||
const noFatherNoDescendant = !hasFather && !hasDescendants;
|
||||
|
||||
if (effMaternalSiblings > 0) {
|
||||
const f = effMaternalSiblings === 1 ? new Fraction(1, 6) : new Fraction(1, 3);
|
||||
shares.push({ heir: 'Maternal (uterine) siblings', count: effMaternalSiblings, fraction: f, note: 'shared equally regardless of sex' });
|
||||
fixedTotal = fixedTotal.add(f);
|
||||
}
|
||||
|
||||
if (noFatherNoDescendant && (effFullBrothers > 0 || effFullSisters > 0)) {
|
||||
if (effFullBrothers === 0 && effFullSisters > 0) {
|
||||
const f = effFullSisters === 1 ? new Fraction(1, 2) : new Fraction(2, 3);
|
||||
shares.push({ heir: effFullSisters === 1 ? 'Full sister' : 'Full sisters (combined)', count: effFullSisters, fraction: f });
|
||||
fixedTotal = fixedTotal.add(f);
|
||||
}
|
||||
// else: full brothers present -> full siblings become 'asabah, handled in residue
|
||||
} else if (noFatherNoDescendant && effPaternalBrothers === 0 && effPaternalSisters > 0) {
|
||||
const f = effPaternalSisters === 1 ? new Fraction(1, 2) : new Fraction(2, 3);
|
||||
shares.push({ heir: effPaternalSisters === 1 ? 'Paternal (consanguine) sister' : 'Paternal (consanguine) sisters (combined)', count: effPaternalSisters, fraction: f });
|
||||
fixedTotal = fixedTotal.add(f);
|
||||
}
|
||||
|
||||
// 'Awl: if fixed shares exceed the whole estate, reduce all fixed shares proportionally.
|
||||
let awlApplied = false;
|
||||
let awlFactor = new Fraction(1, 1);
|
||||
if (fixedTotal.toNumber() > 1) {
|
||||
awlApplied = true;
|
||||
awlFactor = new Fraction(1, 1).div(fixedTotal);
|
||||
for (const s of shares) s.fraction = s.fraction.mul(awlFactor);
|
||||
fixedTotal = new Fraction(1, 1);
|
||||
}
|
||||
|
||||
// Residue to 'asabah (sons+daughters 2:1, else father, else full/paternal brothers+sisters 2:1)
|
||||
let residue = new Fraction(1, 1).sub(fixedTotal);
|
||||
let raddApplied = false;
|
||||
|
||||
if (residue.toNumber() > 0) {
|
||||
if (sons > 0) {
|
||||
const units = sons * 2 + daughters;
|
||||
const sonShare = residue.mul(new Fraction(2, units));
|
||||
const daughterShare = residue.mul(new Fraction(1, units));
|
||||
shares.push({ heir: 'Son(s)', count: sons, fraction: sonShare.mul(new Fraction(sons, 1)), note: '2:1 with daughters, per son total shown' });
|
||||
if (daughters > 0) shares.push({ heir: 'Daughter(s) (residuary share)', count: daughters, fraction: daughterShare.mul(new Fraction(daughters, 1)) });
|
||||
residue = Fraction.zero();
|
||||
}
|
||||
// Father as 'asabah bil-ghayr: with daughters only (no son), father's fixed 1/6 plus any leftover residue.
|
||||
if (hasFather && hasDescendants && sons === 0 && residue.toNumber() > 0) {
|
||||
const fatherShare = shares.find(s => s.heir === 'Father');
|
||||
if (fatherShare) fatherShare.fraction = fatherShare.fraction.add(residue);
|
||||
else shares.push({ heir: 'Father', count: 1, fraction: residue });
|
||||
residue = Fraction.zero();
|
||||
}
|
||||
if (hasFather && !hasDescendants && residue.toNumber() > 0 && !shares.some(s => s.heir === 'Father')) {
|
||||
shares.push({ heir: 'Father (residuary)', count: 1, fraction: residue });
|
||||
residue = Fraction.zero();
|
||||
} else if (noFatherNoDescendant && effFullBrothers > 0 && residue.toNumber() > 0) {
|
||||
const units = effFullBrothers * 2 + effFullSisters;
|
||||
shares.push({ heir: 'Full brother(s)/sister(s) (residuary, 2:1)', count: effFullBrothers + effFullSisters, fraction: residue });
|
||||
residue = Fraction.zero();
|
||||
} else if (noFatherNoDescendant && effPaternalBrothers > 0 && residue.toNumber() > 0) {
|
||||
shares.push({ heir: 'Paternal brother(s)/sister(s) (residuary, 2:1)', count: effPaternalBrothers + effPaternalSisters, fraction: residue });
|
||||
residue = Fraction.zero();
|
||||
}
|
||||
}
|
||||
|
||||
// Radd: leftover residue with no residuary heir returns to fixed-share heirs (excl. spouse) proportionally.
|
||||
if (residue.toNumber() > 0 && shares.length > 0) {
|
||||
raddApplied = true;
|
||||
const raddEligible = shares.filter(s => !s.heir.includes('Wife') && !s.heir.includes('Husband'));
|
||||
const eligibleTotal = raddEligible.reduce((acc, s) => acc.add(s.fraction), Fraction.zero());
|
||||
if (eligibleTotal.toNumber() > 0) {
|
||||
for (const s of raddEligible) {
|
||||
const bonus = s.fraction.div(eligibleTotal).mul(residue);
|
||||
s.fraction = s.fraction.add(bonus);
|
||||
}
|
||||
} else {
|
||||
// no eligible heirs at all besides spouse: spouse takes remainder by radd exception (contested; flagged)
|
||||
const spouseShare = shares.find(s => s.heir.includes('Wife') || s.heir.includes('Husband'));
|
||||
if (spouseShare) { spouseShare.fraction = spouseShare.fraction.add(residue); spouseShare.note = (spouseShare.note || '') + ' [radd-to-spouse: minority position, flag for scholarly review]'; }
|
||||
}
|
||||
residue = Fraction.zero();
|
||||
}
|
||||
|
||||
return {
|
||||
shares: shares.map(s => ({ ...s, fraction: s.fraction.toString(), fractionValue: s.fraction.toNumber() })),
|
||||
awlApplied,
|
||||
raddApplied,
|
||||
isUmariyyatayn
|
||||
};
|
||||
}
|
||||
|
||||
/** Apply computed shares against a currency estate value. */
|
||||
export function applyEstateValue(result, estateValue) {
|
||||
return {
|
||||
...result,
|
||||
shares: result.shares.map(s => ({ ...s, amount: Math.round(s.fractionValue * estateValue * 100) / 100 }))
|
||||
};
|
||||
}
|
||||
|
||||
/** One-third cap check used by Wassiyah, Hibah (marad al-mawt), and Family Waqf (marad al-mawt). */
|
||||
export function oneThirdCap(estateValue) {
|
||||
return Math.round((estateValue / 3) * 100) / 100;
|
||||
}
|
||||
|
||||
export const QURANIC_HEIR_LABELS = [
|
||||
'spouse', 'wife', 'husband', 'son', 'daughter', 'father', 'mother',
|
||||
'full brother', 'full sister', 'paternal brother', 'paternal sister',
|
||||
'maternal brother', 'maternal sister'
|
||||
];
|
||||
|
||||
/** Blocking check for wassiyah/hibah/waqf beneficiary validation: is this relation an existing heir? */
|
||||
export function isQuranicHeirRelation(relation) {
|
||||
const r = (relation || '').toLowerCase();
|
||||
return QURANIC_HEIR_LABELS.some(label => r.includes(label));
|
||||
}
|
||||
Reference in New Issue
Block a user