Files
nur-falah-prevention/src/lib/recommendations.js
T
wmj 7ce1121b94 feat: add stickiness round — daily Sadaqah tracker + Family Tree social loop
Two features scoped from the stickiness brainstorm, sharing one digest
notification pipeline:

Sadaqah tracker (new tab): a private daily giving journal, not a payment
processor — the app logs, it never moves money. Streak tracking follows
the proven pattern from dedicated apps (Sidq, Daily Sadaqa). Family
visibility is strictly limited to streak counts via a SECURITY DEFINER
RPC (nf_family_sadaqah_streaks) — amounts, causes, and notes never cross
the member boundary, respecting the Islamic preference for giving
privately. Entries can be dedicated 'in memory of' a deceased person from
the Family Tree, with a memorial count (nf_memorial_sadaqah_count, count
only) surfacing on that person's card.

Family Tree social loop: a 'give sadaqah in memory of' link on every
deceased person's card (cross-tab jump via a small requestedTab store in
nav.js), plus completeness hints (missing birth date, missing photo,
no relationships linked) feeding directly into the existing Coverage
Dashboard recommendations engine rather than a new UI surface.

Daily digest Edge Function + pg_cron (07:00 UTC): batches into one email
per family member per day, sent only when there's real content — tree
activity in the last 24h, a birthday/death-anniversary today, or a
weekly sadaqah recap on Sundays. An empty day sends nothing, deliberately
avoiding the notification-spam failure mode the research flagged.
Protected by a shared secret header since it's cron-invoked, not
user-triggered. Verified with a live manual invocation before relying on
the schedule.

Found and fixed two real bugs in nf_family_sadaqah_streaks during E2E
testing: an ambiguous unqualified 'member_id' column reference colliding
with the function's OUT parameter (42702), and a bigint/int type mismatch
from count(*) (42804) — both would have 400'd on every call in production.

Covered by e2e-sadaqah-tree.cjs (12/12). Full regression: 280/280 across
all suites.
2026-08-14 13:42:36 +08:00

61 lines
3.9 KiB
JavaScript

// Rule-based guidance engine — turns the app's own data into plain-language
// next steps, the way Legacy Logic's "personalized report" and Rafiq's
// contextual coaching do. Deliberately rule-based against data already
// captured in this app, not a call to an external model: every recommendation
// here traces to a concrete fact (an unverified asset, a missing attestor,
// zero policies logged), not a generated guess.
export function buildRecommendations(data) {
const {
exposedTotal = 0, exposedCount = 0, unverifiedCount = 0, wassiyahCount = 0, estateTotal = 0,
insuranceCount = 0, zakatConfigured = false, hasCashOrGold = false, unlinkedLiabilityCount = 0,
confirmedAttestorCount = 0, hasAgent = false, waqfConfigured = false,
missingBirthDateCount = 0, missingPhotoCount = 0, orphanPersonCount = 0
} = data;
const items = [];
if (exposedTotal > 0) {
items.push({
severity: 'high',
text: `${exposedCount} asset${exposedCount === 1 ? ' is' : 's are'} still exposed to the slow Faraid/probate queue (${exposedTotal.toLocaleString()} total). Cover the highest-value one first — Hibah, Waqf, or a Nomination.`,
tab: 'Coverage'
});
}
if (estateTotal > 0 && wassiyahCount === 0) {
items.push({ severity: 'medium', text: 'No Wassiyah bequests recorded yet. Up to a third of your estate can go to non-heirs or charity — worth setting up even alongside Faraid.', tab: 'Wassiyah' });
}
if (unverifiedCount > 0) {
items.push({ severity: 'medium', text: `${unverifiedCount} asset${unverifiedCount === 1 ? '' : 's'} still unverified — attach proof (title, grant, cover note) and get it confirmed so there's no dispute later.`, tab: 'Assets' });
}
if (unlinkedLiabilityCount > 0) {
items.push({ severity: 'low', text: `${unlinkedLiabilityCount} liabilit${unlinkedLiabilityCount === 1 ? 'y is' : 'ies are'} not linked to the asset securing it — link it so the net estate calculation stays accurate.`, tab: 'Assets' });
}
if (insuranceCount === 0) {
items.push({ severity: 'low', text: 'No life or Takaful policies logged. If you have any, add them — payouts usually go straight to a named beneficiary, outside Faraid entirely.', tab: 'Insurance' });
}
if (hasCashOrGold && !zakatConfigured) {
items.push({ severity: 'low', text: 'You have cash or gold logged but no Zakat calculation set up. Check whether you\'re above Nisab this year.', tab: 'Zakat' });
}
if (confirmedAttestorCount < 2) {
items.push({ severity: 'medium', text: 'Fewer than 2 attestors are confirmed on your death trigger. Without them, your mutawalli/agent can\'t fire the trigger when the time comes.', tab: 'Trigger' });
}
if (!hasAgent) {
items.push({ severity: 'low', text: 'No estate agent/mutawalli assigned to this family yet. Without one, only the owner can execute triggers and confirm assets.', tab: 'Family' });
}
if (estateTotal > 0 && !waqfConfigured) {
items.push({ severity: 'low', text: 'No Waqf designation set up. If any part of your estate is meant as a lasting charitable endowment, this is where to set it aside.', tab: 'Family Waqf' });
}
if (orphanPersonCount > 0) {
items.push({ severity: 'low', text: `${orphanPersonCount} ${orphanPersonCount === 1 ? 'person has' : 'people have'} no relationships linked in your Family Tree — is a parent, child, or spouse missing?`, tab: 'Tree' });
}
if (missingBirthDateCount > 0) {
items.push({ severity: 'low', text: `${missingBirthDateCount} ${missingBirthDateCount === 1 ? 'person is' : 'people are'} missing a birth date in the Family Tree.`, tab: 'Tree' });
}
if (missingPhotoCount > 0) {
items.push({ severity: 'low', text: `${missingPhotoCount} ${missingPhotoCount === 1 ? 'person has' : 'people have'} no photo in the Family Tree.`, tab: 'Tree' });
}
const order = { high: 0, medium: 1, low: 2 };
return items.sort((a, b) => order[a.severity] - order[b.severity]);
}