7ce1121b94
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.
61 lines
3.9 KiB
JavaScript
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]);
|
|
}
|