Files
nur-falah-prevention/src/lib/db.js
wmj 49706e0cd2 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.
2026-08-14 10:17:03 +08:00

493 lines
26 KiB
JavaScript

// Family-scoped data layer over Supabase. Replaces the old per-device localStorage
// calls in storage.js for estate data — Asset Registry, Hibah, Waqf, Nominations,
// Attestors, and the Death Trigger — so an owner and their assigned agent(s) see
// and manage the same live data, with the "agent cannot fire the trigger" rule
// enforced by RLS on nf_death_triggers (see the migration), not just hidden in
// the UI.
import { supabase } from './supabaseClient.js';
// ── Assets ──
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,
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({
family_id: familyId, type: a.type, description: a.description, value: Number(a.value),
location: a.location, ownership_share: Number(a.ownershipShare ?? 100), created_by: userId
}).select().single();
if (error) throw error;
return data;
}
export async function updateAsset(id, a) {
const { error } = await supabase.from('nf_assets').update({
type: a.type, description: a.description, value: Number(a.value), location: a.location, ownership_share: Number(a.ownershipShare ?? 100)
}).eq('id', id);
if (error) throw error;
}
export async function removeAsset(id) {
const { error } = await supabase.from('nf_assets').delete().eq('id', id);
if (error) throw error;
}
// ── Trusted contacts ──
export async function listTrustedContacts(familyId) {
const { data, error } = await supabase.from('nf_trusted_contacts').select('*').eq('family_id', familyId).order('created_at');
if (error) throw error;
return data || [];
}
export async function addTrustedContact(familyId, name, method) {
const { error } = await supabase.from('nf_trusted_contacts').insert({ family_id: familyId, name, method });
if (error) throw error;
}
export async function removeTrustedContact(id) {
const { error } = await supabase.from('nf_trusted_contacts').delete().eq('id', id);
if (error) throw error;
}
// ── Hibah ──
export async function listHibahGifts(familyId) {
const { data, error } = await supabase.from('nf_hibah_gifts').select('*').eq('family_id', familyId).order('created_at');
if (error) throw error;
return (data || []).map(g => ({ id: g.id, recipient: g.recipient, relation: g.relation, description: g.description, date: g.gift_date, statement: g.statement, linkedAssetId: g.linked_asset_id, flagged: g.flagged, cap: g.cap }));
}
export async function addHibahGift(familyId, g) {
const { error } = await supabase.from('nf_hibah_gifts').insert({
family_id: familyId, recipient: g.recipient, relation: g.relation, description: g.description,
gift_date: g.date, statement: g.statement, linked_asset_id: g.linkedAssetId || null,
flagged: !!g.flagged, cap: g.cap ?? null, acknowledgment_link: g.acknowledgmentLink
});
if (error) throw error;
}
export async function removeHibahGift(id) {
const { error } = await supabase.from('nf_hibah_gifts').delete().eq('id', id);
if (error) throw error;
}
// ── Waqf ──
export async function getWaqfDesignation(familyId, authorId) {
const { data, error } = await supabase.from('nf_waqf_designations').select('*').eq('family_id', familyId).eq('author_id', authorId).order('created_at', { ascending: false }).limit(1).maybeSingle();
if (error) throw error;
if (!data) return null;
const { data: beneficiaries } = await supabase.from('nf_waqf_beneficiaries').select('*').eq('waqf_id', data.id);
return {
id: data.id, corpusAssetId: data.corpus_asset_id, mutawalli: data.mutawalli,
successorMutawalli: data.successor_mutawalli, equalSplit: data.equal_split, jurisdiction: data.jurisdiction,
beneficiaries: (beneficiaries || []).map(b => ({ id: b.id, name: b.name, relation: b.relation, sharePercent: b.share_percent, email: b.beneficiary_email }))
};
}
export async function upsertWaqfDesignation(familyId, authorId, existingId, fields) {
if (existingId) {
const { error } = await supabase.from('nf_waqf_designations').update({
corpus_asset_id: fields.corpusAssetId || null, mutawalli: fields.mutawalli, successor_mutawalli: fields.successorMutawalli,
equal_split: fields.equalSplit, jurisdiction: fields.jurisdiction
}).eq('id', existingId);
if (error) throw error;
return existingId;
}
const { data, error } = await supabase.from('nf_waqf_designations').insert({
family_id: familyId, author_id: authorId, corpus_asset_id: fields.corpusAssetId || null, mutawalli: fields.mutawalli,
successor_mutawalli: fields.successorMutawalli, equal_split: fields.equalSplit, jurisdiction: fields.jurisdiction
}).select().single();
if (error) throw error;
return data.id;
}
export async function addWaqfBeneficiary(waqfId, b) {
const { error } = await supabase.from('nf_waqf_beneficiaries').insert({ waqf_id: waqfId, name: b.name, relation: b.relation, share_percent: b.sharePercent || null, beneficiary_email: b.email || null });
if (error) throw error;
}
/** For the mutawalli dashboard: every family member's Waqf designations. */
export async function listAllWaqfForFamily(familyId) {
const { data, error } = await supabase.from('nf_waqf_designations').select('*, nf_waqf_beneficiaries(*)').eq('family_id', familyId).order('created_at');
if (error) throw error;
return data || [];
}
export async function removeWaqfBeneficiary(id) {
const { error } = await supabase.from('nf_waqf_beneficiaries').delete().eq('id', id);
if (error) throw error;
}
// ── Nominations ──
export async function listNominations(familyId) {
const { data, error } = await supabase.from('nf_nominations').select('*').eq('family_id', familyId).order('created_at');
if (error) throw error;
return (data || []).map(n => ({
id: n.id, linkedAssetId: n.linked_asset_id, type: n.type, institution: n.institution, nomineeeName: n.nominee_name,
referenceNumber: n.reference_number, trusteeName: n.trustee_name, successorTrustee: n.successor_trustee,
trustBeneficiaries: n.trust_beneficiaries, businessStructure: n.business_structure, businessInstrument: n.business_instrument,
successorOwner: n.successor_owner, buySellTerms: n.buy_sell_terms, custodyType: n.custody_type, platform: n.platform,
keyHolderName: n.key_holder_name, accessInstructionsRef: n.access_instructions_ref
}));
}
export async function addNomination(familyId, n) {
const { error } = await supabase.from('nf_nominations').insert({
family_id: familyId, linked_asset_id: n.linkedAssetId || null, type: n.type, institution: n.institution,
nominee_name: n.nomineeeName, reference_number: n.referenceNumber, trustee_name: n.trusteeName,
successor_trustee: n.successorTrustee, trust_beneficiaries: n.trustBeneficiaries, business_structure: n.businessStructure,
business_instrument: n.businessInstrument, successor_owner: n.successorOwner, buy_sell_terms: n.buySellTerms,
custody_type: n.custodyType, platform: n.platform, key_holder_name: n.keyHolderName, access_instructions_ref: n.accessInstructionsRef
});
if (error) throw error;
}
export async function removeNomination(id) {
const { error } = await supabase.from('nf_nominations').delete().eq('id', id);
if (error) throw error;
}
// ── Attestors & Death Trigger ──
export async function listAttestors(familyId) {
const { data, error } = await supabase.from('nf_attestors').select('*').eq('family_id', familyId).order('created_at');
if (error) throw error;
return data || [];
}
export async function addAttestor(familyId, name) {
const { data, error } = await supabase.from('nf_attestors').insert({ family_id: familyId, name }).select().single();
if (error) throw error;
return data;
}
export async function setAttestorConfirmed(id, confirmed) {
const { error } = await supabase.from('nf_attestors').update({ confirmed, confirmed_at: confirmed ? new Date().toISOString() : null }).eq('id', id);
if (error) throw error;
}
export async function updateAttestorName(id, name) {
const { error } = await supabase.from('nf_attestors').update({ name }).eq('id', id);
if (error) throw error;
}
export async function getDeathTrigger(familyId) {
const { data, error } = await supabase.from('nf_death_triggers').select('*').eq('family_id', familyId).maybeSingle();
if (error) throw error;
return data;
}
export async function upsertDeathTriggerSetup(familyId, fields) {
const { error } = await supabase.from('nf_death_triggers').upsert({
family_id: familyId, executor_name: fields.executorName, date_of_death: fields.dateOfDeath || null,
death_cert_ref: fields.deathCertRef, updated_at: new Date().toISOString()
}, { onConflict: 'family_id' });
if (error) throw error;
}
/**
* Updates a single death-trigger setup field. Used instead of
* upsertDeathTriggerSetup for per-keystroke autosave on individual inputs —
* three fields each firing their own full-snapshot upsert can complete
* out of order over the network, and the last one to *finish* (not the last
* one to *fire*) silently clobbers the other two fields back to whatever
* stale value it captured. A single-column update can't do that.
*/
export async function updateDeathTriggerField(familyId, column, value) {
const { error } = await supabase.from('nf_death_triggers').upsert({
family_id: familyId, [column]: value, updated_at: new Date().toISOString()
}, { onConflict: 'family_id' });
if (error) throw error;
}
/** Only succeeds (per RLS) if the caller has role='owner' on this family. */
export async function fireDeathTrigger(familyId) {
const { error } = await supabase.from('nf_death_triggers').upsert({
family_id: familyId, triggered: true, triggered_at: new Date().toISOString(), updated_at: new Date().toISOString()
}, { onConflict: 'family_id' });
if (error) throw error;
}
export function estateTotal(assets) {
return (assets || []).reduce((sum, a) => sum + (Number(a.value) || 0) * (Number(a.ownershipShare ?? 100) / 100), 0);
}
// ── Wassiyah — per-author: each signed-in family member has their own ──
export async function listWassiyahBequests(familyId, authorId) {
const { data, error } = await supabase.from('nf_wassiyah_bequests').select('*').eq('family_id', familyId).eq('author_id', authorId).order('created_at');
if (error) throw error;
return (data || []).map(b => ({ id: b.id, recipient: b.recipient, relation: b.relation, description: b.description, value: b.value, recipientEmail: b.recipient_email }));
}
export async function addWassiyahBequest(familyId, authorId, b) {
const { error } = await supabase.from('nf_wassiyah_bequests').insert({
family_id: familyId, author_id: authorId, recipient: b.recipient, relation: b.relation,
description: b.description, value: Number(b.value), recipient_email: b.recipientEmail || null
});
if (error) throw error;
}
export async function removeWassiyahBequest(id) {
const { error } = await supabase.from('nf_wassiyah_bequests').delete().eq('id', id);
if (error) throw error;
}
export async function getWassiyahSettings(familyId, authorId) {
const { data, error } = await supabase.from('nf_wassiyah_settings').select('*').eq('family_id', familyId).eq('author_id', authorId).maybeSingle();
if (error) throw error;
return data;
}
export async function upsertWassiyahSettings(familyId, authorId, fields) {
const { error } = await supabase.from('nf_wassiyah_settings').upsert({
family_id: familyId, author_id: authorId, jurisdiction: fields.jurisdiction, witness1: fields.witness1, witness2: fields.witness2,
updated_at: new Date().toISOString()
}, { onConflict: 'family_id,author_id' });
if (error) throw error;
}
/** For the mutawalli dashboard: every family member's Wassiyah bequests, grouped by author. */
export async function listAllWassiyahForFamily(familyId) {
const { data, error } = await supabase.from('nf_wassiyah_bequests').select('*').eq('family_id', familyId).order('created_at');
if (error) throw error;
return data || [];
}
// ── Per-member death trigger — the mutawalli/owner fires it for a specific
// member, never the member themselves (enforced by RLS, not just the UI). ──
export async function getMemberTrigger(memberId, familyId) {
const { data, error } = await supabase.from('nf_member_triggers').select('*').eq('member_id', memberId).eq('family_id', familyId).maybeSingle();
if (error) throw error;
return data;
}
export async function upsertMemberTriggerSetup(memberId, familyId, fields) {
const { error } = await supabase.from('nf_member_triggers').upsert({
member_id: memberId, family_id: familyId, executor_name: fields.executorName,
date_of_death: fields.dateOfDeath || null, death_cert_ref: fields.deathCertRef, updated_at: new Date().toISOString()
}, { onConflict: 'family_id,member_id' });
if (error) throw error;
}
/** Only succeeds (per RLS) if the caller is an agent/owner on this family and is NOT the member themselves. */
export async function fireMemberTrigger(memberId, familyId, firedBy) {
const { error } = await supabase.from('nf_member_triggers').upsert({
member_id: memberId, family_id: familyId, triggered: true, triggered_at: new Date().toISOString(),
fired_by: firedBy, updated_at: new Date().toISOString()
}, { onConflict: 'family_id,member_id' });
if (error) throw error;
}
export async function listMemberAttestors(memberId, familyId) {
const { data, error } = await supabase.from('nf_member_attestors').select('*').eq('member_id', memberId).eq('family_id', familyId).order('created_at');
if (error) throw error;
return data || [];
}
export async function addMemberAttestor(memberId, familyId, name) {
const { data, error } = await supabase.from('nf_member_attestors').insert({ member_id: memberId, family_id: familyId, name }).select().single();
if (error) throw error;
return data;
}
export async function updateMemberAttestorName(id, name) {
const { error } = await supabase.from('nf_member_attestors').update({ name }).eq('id', id);
if (error) throw error;
}
export async function setMemberAttestorConfirmed(id, confirmed) {
const { error } = await supabase.from('nf_member_attestors').update({ confirmed, confirmed_at: confirmed ? new Date().toISOString() : null }).eq('id', id);
if (error) throw error;
}
/** Sends the heir notification email via the notify-heirs Edge Function. */
export async function notifyHeirs(memberId, familyId) {
const { data, error } = await supabase.functions.invoke('notify-heirs', { body: { memberId, familyId } });
if (error) throw error;
return data;
}
// ── Family Genealogy ──
// nf_people are lightweight — most real tree nodes (grandparents, deceased
// relatives, young children) never have a login. linked_user_id is optional.
const PHOTO_BUCKET = 'nf-people-photos';
export async function listPeople(familyId) {
const { data, error } = await supabase.from('nf_people').select('*').eq('family_id', familyId).order('created_at');
if (error) throw error;
return (data || []).map(p => ({
id: p.id, familyId: p.family_id, linkedUserId: p.linked_user_id, fullName: p.full_name,
gender: p.gender, birthDate: p.birth_date, deathDate: p.death_date, notes: p.notes, photoPath: p.photo_path
}));
}
export async function addPerson(familyId, createdBy, fields) {
const { data, error } = await supabase.from('nf_people').insert({
family_id: familyId, created_by: createdBy, full_name: fields.fullName, gender: fields.gender || null,
birth_date: fields.birthDate || null, death_date: fields.deathDate || null, notes: fields.notes || null
}).select().single();
if (error) throw error;
return data;
}
export async function updatePerson(id, fields) {
const { error } = await supabase.from('nf_people').update({
full_name: fields.fullName, gender: fields.gender || null, birth_date: fields.birthDate || null,
death_date: fields.deathDate || null, notes: fields.notes || null, updated_at: new Date().toISOString()
}).eq('id', id);
if (error) throw error;
}
export async function removePerson(id) {
const { error } = await supabase.from('nf_people').delete().eq('id', id);
if (error) throw error;
}
/** Uploads a photo for a person and stores its storage path on the record. Path: {familyId}/{personId}/{filename} so RLS can check family membership from the path alone. */
export async function uploadPersonPhoto(familyId, personId, file) {
const ext = (file.name.split('.').pop() || 'jpg').toLowerCase();
const path = `${familyId}/${personId}/${Date.now()}.${ext}`;
const { error: upErr } = await supabase.storage.from(PHOTO_BUCKET).upload(path, file, { upsert: true, contentType: file.type });
if (upErr) throw upErr;
const { error: dbErr } = await supabase.from('nf_people').update({ photo_path: path, updated_at: new Date().toISOString() }).eq('id', personId);
if (dbErr) throw dbErr;
return path;
}
export async function removePersonPhoto(personId, photoPath) {
if (photoPath) await supabase.storage.from(PHOTO_BUCKET).remove([photoPath]);
const { error } = await supabase.from('nf_people').update({ photo_path: null, updated_at: new Date().toISOString() }).eq('id', personId);
if (error) throw error;
}
/** Signed URL (private bucket) valid for 1 hour — call fresh each time the photo is displayed. */
export async function getPersonPhotoUrl(photoPath) {
if (!photoPath) return null;
const { data, error } = await supabase.storage.from(PHOTO_BUCKET).createSignedUrl(photoPath, 3600);
if (error) return null;
return data.signedUrl;
}
export async function listRelationships(familyId) {
const { data, error } = await supabase.from('nf_relationships').select('*').eq('family_id', familyId);
if (error) throw error;
return (data || []).map(r => ({ id: r.id, personAId: r.person_a_id, personBId: r.person_b_id, type: r.relationship_type }));
}
export async function addRelationship(familyId, personAId, personBId, type) {
const { error } = await supabase.from('nf_relationships').insert({ family_id: familyId, person_a_id: personAId, person_b_id: personBId, relationship_type: type });
if (error) throw error;
}
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;
}