// 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)); }