feat: add insurance/Takaful tracking, liabilities, and asset ownership verification
Per-member Insurance & Takaful tab (life/Takaful/medical/asset policies, same author-owns/family-reads pattern as Wassiyah/Waqf), a family-shared Liabilities section on the Asset Registry (kept distinct from assets since Faraid requires debts settled before distribution), and dual-path asset ownership verification (proof document + confirm by either the mutawalli/agent or any other family member, since not every demo family has an agent assigned). All proof documents share a new private nf-asset-documents storage bucket with the same RLS pattern as person photos. Mutawalli dashboard now surfaces each member's insurance policies. Covered by e2e-insurance-verification.cjs (12/12); full regression sweep 233/233 across all suites.
This commit is contained in:
+136
-1
@@ -10,7 +10,10 @@ import { supabase } from './supabaseClient.js';
|
||||
export async function listAssets(familyId) {
|
||||
const { data, error } = await supabase.from('nf_assets').select('*').eq('family_id', familyId).order('created_at');
|
||||
if (error) throw error;
|
||||
return (data || []).map(a => ({ id: a.id, type: a.type, description: a.description, value: a.value, location: a.location, ownershipShare: a.ownership_share }));
|
||||
return (data || []).map(a => ({
|
||||
id: a.id, type: a.type, description: a.description, value: a.value, location: a.location, ownershipShare: a.ownership_share,
|
||||
verified: a.verified, verifiedBy: a.verified_by, verifiedAt: a.verified_at, proofDocumentPath: a.proof_document_path
|
||||
}));
|
||||
}
|
||||
export async function addAsset(familyId, userId, a) {
|
||||
const { data, error } = await supabase.from('nf_assets').insert({
|
||||
@@ -355,3 +358,135 @@ export async function removeRelationship(id) {
|
||||
const { error } = await supabase.from('nf_relationships').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
// ── Insurance / Takaful — per-member: each family member logs their own
|
||||
// policies, but any family member (and the mutawalli dashboard) can read them. ──
|
||||
export async function listInsurancePolicies(familyId, memberId) {
|
||||
const { data, error } = await supabase.from('nf_insurance_policies').select('*').eq('family_id', familyId).eq('member_id', memberId).order('created_at');
|
||||
if (error) throw error;
|
||||
return (data || []).map(p => ({
|
||||
id: p.id, policyType: p.policy_type, provider: p.provider, policyNumber: p.policy_number,
|
||||
sumAssured: p.sum_assured, beneficiaryName: p.beneficiary_name, beneficiaryRelation: p.beneficiary_relation,
|
||||
expiryDate: p.expiry_date, notes: p.notes, documentPath: p.document_path
|
||||
}));
|
||||
}
|
||||
export async function addInsurancePolicy(familyId, memberId, createdBy, p) {
|
||||
const { data, error } = await supabase.from('nf_insurance_policies').insert({
|
||||
family_id: familyId, member_id: memberId, created_by: createdBy, policy_type: p.policyType,
|
||||
provider: p.provider, policy_number: p.policyNumber, sum_assured: p.sumAssured ? Number(p.sumAssured) : null,
|
||||
beneficiary_name: p.beneficiaryName, beneficiary_relation: p.beneficiaryRelation, expiry_date: p.expiryDate || null, notes: p.notes
|
||||
}).select().single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
export async function updateInsurancePolicy(id, p) {
|
||||
const { error } = await supabase.from('nf_insurance_policies').update({
|
||||
policy_type: p.policyType, provider: p.provider, policy_number: p.policyNumber,
|
||||
sum_assured: p.sumAssured ? Number(p.sumAssured) : null, beneficiary_name: p.beneficiaryName,
|
||||
beneficiary_relation: p.beneficiaryRelation, expiry_date: p.expiryDate || null, notes: p.notes,
|
||||
updated_at: new Date().toISOString()
|
||||
}).eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
export async function removeInsurancePolicy(id) {
|
||||
const { error } = await supabase.from('nf_insurance_policies').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
/** For the mutawalli dashboard: every family member's insurance/Takaful policies. */
|
||||
export async function listAllInsuranceForFamily(familyId) {
|
||||
const { data, error } = await supabase.from('nf_insurance_policies').select('*').eq('family_id', familyId).order('created_at');
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
}
|
||||
|
||||
const POLICY_DOC_BUCKET = 'nf-asset-documents';
|
||||
export async function uploadPolicyDocument(familyId, policyId, file) {
|
||||
const ext = (file.name.split('.').pop() || 'pdf').toLowerCase();
|
||||
const path = `${familyId}/policy-${policyId}/${Date.now()}.${ext}`;
|
||||
const { error: upErr } = await supabase.storage.from(POLICY_DOC_BUCKET).upload(path, file, { upsert: true, contentType: file.type });
|
||||
if (upErr) throw upErr;
|
||||
const { error: dbErr } = await supabase.from('nf_insurance_policies').update({ document_path: path, updated_at: new Date().toISOString() }).eq('id', policyId);
|
||||
if (dbErr) throw dbErr;
|
||||
return path;
|
||||
}
|
||||
export async function removePolicyDocument(policyId, docPath) {
|
||||
if (docPath) await supabase.storage.from(POLICY_DOC_BUCKET).remove([docPath]);
|
||||
const { error } = await supabase.from('nf_insurance_policies').update({ document_path: null, updated_at: new Date().toISOString() }).eq('id', policyId);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
// ── Liabilities — family-shared, same ownership model as Assets. Kept
|
||||
// distinct because Faraid requires debts settled before distribution. ──
|
||||
export async function listLiabilities(familyId) {
|
||||
const { data, error } = await supabase.from('nf_liabilities').select('*').eq('family_id', familyId).order('created_at');
|
||||
if (error) throw error;
|
||||
return (data || []).map(l => ({
|
||||
id: l.id, liabilityType: l.liability_type, lender: l.lender, outstandingBalance: l.outstanding_balance,
|
||||
linkedAssetId: l.linked_asset_id, notes: l.notes, documentPath: l.document_path
|
||||
}));
|
||||
}
|
||||
export async function addLiability(familyId, createdBy, l) {
|
||||
const { data, error } = await supabase.from('nf_liabilities').insert({
|
||||
family_id: familyId, created_by: createdBy, liability_type: l.liabilityType, lender: l.lender,
|
||||
outstanding_balance: l.outstandingBalance ? Number(l.outstandingBalance) : null,
|
||||
linked_asset_id: l.linkedAssetId || null, notes: l.notes
|
||||
}).select().single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
export async function updateLiability(id, l) {
|
||||
const { error } = await supabase.from('nf_liabilities').update({
|
||||
liability_type: l.liabilityType, lender: l.lender, outstanding_balance: l.outstandingBalance ? Number(l.outstandingBalance) : null,
|
||||
linked_asset_id: l.linkedAssetId || null, notes: l.notes, updated_at: new Date().toISOString()
|
||||
}).eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
export async function removeLiability(id) {
|
||||
const { error } = await supabase.from('nf_liabilities').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
export async function uploadLiabilityDocument(familyId, liabilityId, file) {
|
||||
const ext = (file.name.split('.').pop() || 'pdf').toLowerCase();
|
||||
const path = `${familyId}/liability-${liabilityId}/${Date.now()}.${ext}`;
|
||||
const { error: upErr } = await supabase.storage.from(POLICY_DOC_BUCKET).upload(path, file, { upsert: true, contentType: file.type });
|
||||
if (upErr) throw upErr;
|
||||
const { error: dbErr } = await supabase.from('nf_liabilities').update({ document_path: path, updated_at: new Date().toISOString() }).eq('id', liabilityId);
|
||||
if (dbErr) throw dbErr;
|
||||
return path;
|
||||
}
|
||||
export async function removeLiabilityDocument(liabilityId, docPath) {
|
||||
if (docPath) await supabase.storage.from(POLICY_DOC_BUCKET).remove([docPath]);
|
||||
const { error } = await supabase.from('nf_liabilities').update({ document_path: null, updated_at: new Date().toISOString() }).eq('id', liabilityId);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
// ── Asset ownership verification — a proof document (car grant, land title,
|
||||
// cover note, loan agreement) plus a dual-path confirm: either the
|
||||
// mutawalli/agent OR any other family member can mark an asset verified. ──
|
||||
export async function uploadAssetDocument(familyId, assetId, file) {
|
||||
const ext = (file.name.split('.').pop() || 'pdf').toLowerCase();
|
||||
const path = `${familyId}/asset-${assetId}/${Date.now()}.${ext}`;
|
||||
const { error: upErr } = await supabase.storage.from(POLICY_DOC_BUCKET).upload(path, file, { upsert: true, contentType: file.type });
|
||||
if (upErr) throw upErr;
|
||||
const { error: dbErr } = await supabase.from('nf_assets').update({ proof_document_path: path }).eq('id', assetId);
|
||||
if (dbErr) throw dbErr;
|
||||
return path;
|
||||
}
|
||||
export async function removeAssetDocument(assetId, docPath) {
|
||||
if (docPath) await supabase.storage.from(POLICY_DOC_BUCKET).remove([docPath]);
|
||||
const { error } = await supabase.from('nf_assets').update({ proof_document_path: null }).eq('id', assetId);
|
||||
if (error) throw error;
|
||||
}
|
||||
export async function getDocumentUrl(path) {
|
||||
if (!path) return null;
|
||||
const { data, error } = await supabase.storage.from(POLICY_DOC_BUCKET).createSignedUrl(path, 3600);
|
||||
if (error) return null;
|
||||
return data.signedUrl;
|
||||
}
|
||||
/** Verifying isn't restricted to the mutawalli — any other family member who has seen the proof can confirm. */
|
||||
export async function setAssetVerified(assetId, verifiedBy, verified) {
|
||||
const { error } = await supabase.from('nf_assets').update({
|
||||
verified, verified_by: verified ? verifiedBy : null, verified_at: verified ? new Date().toISOString() : null
|
||||
}).eq('id', assetId);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user