6690696f7f
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.
231 lines
11 KiB
JavaScript
231 lines
11 KiB
JavaScript
// 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));
|
|
}
|