feat: close 3 more competitive gaps — recommendations engine, multi-madhab Faraid, draft will PDF
Closes 3 of the remaining 4 gaps from the competitive benchmark: - Coverage Dashboard now has a rule-based recommendations engine (recommendations.js) surfacing plain-language next steps from data already in the app — exposed assets, missing Wassiyah, unverified assets, unlinked liabilities, missing insurance/Zakat setup, insufficient attestors, no agent assigned, no Waqf configured. - Faraid Calculator gains a madhab selector (Shafi'i/Hanafi/Maliki/ Hanbali) encoding the one well-documented divergence this engine's existing rules touch: whether radd extends to a sole-heir spouse (Hanafi: yes; Shafi'i/Maliki/Hanbali: no, residue unallocated). Ja'fari (Shia) is honestly gated as unsupported rather than silently computed with Sunni rules, since it's a structurally different classification system, not a parameter tweak. faraid.test.js grows from 14 to 17 cases covering the divergence and the gating. - Wassiyah tab gains a 'Generate draft will document' button producing a formatted, statutory-style DRAFT will (declaration/revocation, executor appointment, bequest schedule, witness attestation blocks) via browser print-to-PDF — no new PDF dependency. Clearly watermarked 'DRAFT — NOT EXECUTED, requires physical signing and witnessing.' Not a claim of legal validity, a lawyer-reviewable starting point. The 4th gap (human-in-the-loop professional tier) remains open — out of scope for a self-serve prevention tool. COMPETITIVE_BENCHMARK.md updated to reflect closed/partially-closed status on each item. Covered by e2e-gaps-round2.cjs (14/14) plus 3 new faraid.test.js cases. Full regression: 226/226 across all suites.
This commit is contained in:
+33
-5
@@ -7,6 +7,18 @@
|
||||
// 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.
|
||||
//
|
||||
// Madhab scope: this engine's default rules match the Shafi'i-aligned majority
|
||||
// position used in Malaysian statutory Faraid application, which Maliki and
|
||||
// Hanbali also follow on the one point this engine models a real divergence
|
||||
// on. Hanafi fiqh diverges on radd (see below). Ja'fari (Shia) inheritance
|
||||
// uses a fundamentally different classification system — not a parameter
|
||||
// tweak on this engine — and is deliberately NOT computed here; see
|
||||
// SUPPORTED_MADHABS and the 'jaafari' branch in calculateFaraid.
|
||||
export const SUPPORTED_MADHABS = ['shafii', 'hanafi', 'maliki', 'hanbali'];
|
||||
export const MADHAB_LABELS = {
|
||||
shafii: "Shafi'i", hanafi: 'Hanafi', maliki: 'Maliki', hanbali: 'Hanbali', jaafari: "Ja'fari (Shia)"
|
||||
};
|
||||
|
||||
function gcd(a, b) { return b === 0 ? a : gcd(b, a % b); }
|
||||
|
||||
@@ -32,8 +44,12 @@ class Fraction {
|
||||
* spouseCount, deceasedGender ('male'|'female'),
|
||||
* sons, daughters, father, mother (bool),
|
||||
* fullBrothers, fullSisters, paternalBrothers, paternalSisters, maternalSiblings
|
||||
* @param {string} madhab one of SUPPORTED_MADHABS, or 'jaafari' (returns unsupported: true instead of shares)
|
||||
*/
|
||||
export function calculateFaraid(heirs) {
|
||||
export function calculateFaraid(heirs, madhab = 'shafii') {
|
||||
if (madhab === 'jaafari') {
|
||||
return { unsupported: true, madhab, shares: [], awlApplied: false, raddApplied: false, isUmariyyatayn: false };
|
||||
}
|
||||
const {
|
||||
deceasedGender = 'male',
|
||||
spouseCount = 0,
|
||||
@@ -189,18 +205,30 @@ export function calculateFaraid(heirs) {
|
||||
s.fraction = s.fraction.add(bonus);
|
||||
}
|
||||
} else {
|
||||
// no eligible heirs at all besides spouse: spouse takes remainder by radd exception (contested; flagged)
|
||||
// No eligible heirs at all besides spouse: whether the spouse absorbs the
|
||||
// remainder by radd is where Hanafi fiqh actually diverges from the
|
||||
// Shafi'i-aligned majority position this engine otherwise follows.
|
||||
// Hanafi: spouse included in radd, takes the full remainder.
|
||||
// Shafi'i/Maliki/Hanbali: spouse excluded from radd — remainder is not
|
||||
// distributed to any private heir (classically: Bayt al-Mal).
|
||||
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]'; }
|
||||
if (madhab === 'hanafi' && spouseShare) {
|
||||
spouseShare.fraction = spouseShare.fraction.add(residue);
|
||||
spouseShare.note = (spouseShare.note || '') + ' [Hanafi: radd extends to spouse]';
|
||||
residue = Fraction.zero();
|
||||
} else if (spouseShare) {
|
||||
spouseShare.note = (spouseShare.note || '') + ` [${MADHAB_LABELS[madhab]}: spouse excluded from radd — remainder held for public treasury/Bayt al-Mal, not distributed to a private heir]`;
|
||||
}
|
||||
}
|
||||
residue = Fraction.zero();
|
||||
}
|
||||
|
||||
return {
|
||||
shares: shares.map(s => ({ ...s, fraction: s.fraction.toString(), fractionValue: s.fraction.toNumber() })),
|
||||
awlApplied,
|
||||
raddApplied,
|
||||
isUmariyyatayn
|
||||
isUmariyyatayn,
|
||||
madhab,
|
||||
unallocatedResidue: residue.toNumber() > 0 ? residue.toNumber() : 0
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -105,5 +105,30 @@ check('Mother + father only, no spouse/children/siblings', {
|
||||
mother: true, father: true
|
||||
}, { 'Mother': 1 / 3, 'Father (residuary)': 2 / 3 });
|
||||
|
||||
// 12. Madhab divergence — wife is the sole heir (no other heirs at all): Hanafi
|
||||
// extends radd to the spouse (wife takes 100%); Shafi'i/Maliki/Hanbali exclude
|
||||
// the spouse from radd (wife keeps her fixed 1/4, remainder unallocated).
|
||||
{
|
||||
const hanafi = calculateFaraid({ spouseCount: 1, deceasedGender: 'male' }, 'hanafi');
|
||||
const wifeHanafi = hanafi.shares.find(s => s.heir.includes('Wife'));
|
||||
if (wifeHanafi && approx(wifeHanafi.fractionValue, 1) && hanafi.unallocatedResidue === 0) {
|
||||
pass++; console.log('PASS Madhab: Hanafi extends radd to sole-heir spouse (100%)');
|
||||
} else { fail++; console.log('FAIL Madhab: Hanafi radd-to-spouse', JSON.stringify(hanafi)); }
|
||||
|
||||
const shafii = calculateFaraid({ spouseCount: 1, deceasedGender: 'male' }, 'shafii');
|
||||
const wifeShafii = shafii.shares.find(s => s.heir.includes('Wife'));
|
||||
if (wifeShafii && approx(wifeShafii.fractionValue, 1 / 4) && approx(shafii.unallocatedResidue, 3 / 4)) {
|
||||
pass++; console.log("PASS Madhab: Shafi'i excludes spouse from radd (wife keeps 1/4, 3/4 unallocated)");
|
||||
} else { fail++; console.log("FAIL Madhab: Shafi'i radd-to-spouse exclusion", JSON.stringify(shafii)); }
|
||||
}
|
||||
|
||||
// 13. Ja'fari is honestly gated as unsupported, not silently computed with Sunni rules.
|
||||
{
|
||||
const jaafari = calculateFaraid({ spouseCount: 1, deceasedGender: 'male' }, 'jaafari');
|
||||
if (jaafari.unsupported === true && jaafari.shares.length === 0) {
|
||||
pass++; console.log("PASS Madhab: Ja'fari (Shia) is gated as unsupported, not silently miscalculated");
|
||||
} else { fail++; console.log("FAIL Madhab: Ja'fari gating", JSON.stringify(jaafari)); }
|
||||
}
|
||||
|
||||
console.log(`\n${pass} passed, ${fail} failed`);
|
||||
if (fail > 0) process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user