feat: add Zakat calculator (per-member, Nisab + 2.5% rate)

Closes the Zakat gap identified in the competitive benchmark against
Rafiq. Per-member module (nf_zakat_records, same author-owns/family-reads
RLS pattern as Insurance/Wassiyah/Waqf) computing 2.5% due on zakatable
wealth (cash, gold, silver, business assets, investments) once above a
user-entered Nisab threshold, minus deductible short-term liabilities.
Manual entry only, matching the app's existing philosophy — no live gold
price feed. Covered by e2e-zakat.cjs (7/7).
This commit is contained in:
2026-08-14 11:55:59 +08:00
parent 2b022cbcc3
commit 972ad7ff68
5 changed files with 234 additions and 12 deletions
+22
View File
@@ -490,3 +490,25 @@ export async function setAssetVerified(assetId, verifiedBy, verified) {
}).eq('id', assetId);
if (error) throw error;
}
// ── Zakat — per-member, one live record per family/member pair. ──
export async function getZakatRecord(familyId, memberId) {
const { data, error } = await supabase.from('nf_zakat_records').select('*').eq('family_id', familyId).eq('member_id', memberId).maybeSingle();
if (error) throw error;
if (!data) return null;
return {
cash: data.cash, gold: data.gold, silver: data.silver, businessAssets: data.business_assets,
investments: data.investments, otherZakatable: data.other_zakatable,
deductibleLiabilities: data.deductible_liabilities, nisabThreshold: data.nisab_threshold
};
}
export async function upsertZakatRecord(familyId, memberId, fields) {
const { error } = await supabase.from('nf_zakat_records').upsert({
family_id: familyId, member_id: memberId,
cash: Number(fields.cash) || 0, gold: Number(fields.gold) || 0, silver: Number(fields.silver) || 0,
business_assets: Number(fields.businessAssets) || 0, investments: Number(fields.investments) || 0,
other_zakatable: Number(fields.otherZakatable) || 0, deductible_liabilities: Number(fields.deductibleLiabilities) || 0,
nisab_threshold: Number(fields.nisabThreshold) || 0, updated_at: new Date().toISOString()
}, { onConflict: 'family_id,member_id' });
if (error) throw error;
}