/** * Formatting. Indian conventions throughout, because the numbers in this app * are rupee amounts read by Indian bankers. * * `en-IN` grouping is not cosmetic: ₹18,00,000 is how eighteen lakh is written, * and ₹1,800,000 reads as a foreign system's idea of the same number. The * lakh/crore short forms exist because a queue column has no room for eight * digits and "₹18.0 L" is what a person would say out loud. */ const INR = new Intl.NumberFormat('en-IN', { maximumFractionDigits: 0 }); export function num(v: unknown): number | null { if (v === null || v === undefined || v === '') return null; const n = Number(v); return Number.isFinite(n) ? n : null; } export function str(v: unknown): string { if (v === null || v === undefined) return ''; return String(v); } /** ₹18,00,000 */ export function money(v: unknown): string { const n = num(v); return n === null ? '—' : `₹${INR.format(n)}`; } /** ₹18.0 L / ₹1.25 Cr — for columns and tiles. */ export function moneyShort(v: unknown): string { const n = num(v); if (n === null) return '—'; if (Math.abs(n) >= 1e7) return `₹${(n / 1e7).toFixed(2)} Cr`; if (Math.abs(n) >= 1e5) return `₹${(n / 1e5).toFixed(1)} L`; return `₹${INR.format(n)}`; } /** 44.3% */ export function pct(v: unknown, digits = 1): string { const n = num(v); return n === null ? '—' : `${n.toFixed(digits)}%`; } /** A stored ratio (0.7445) shown as a percentage. */ export function ratioPct(v: unknown, digits = 1): string { const n = num(v); return n === null ? '—' : `${(n * 100).toFixed(digits)}%`; } /** 1.34 */ export function ratio(v: unknown, digits = 2): string { const n = num(v); return n === null ? '—' : n.toFixed(digits); } export function dateTime(v: unknown): string { const s = str(v); if (!s) return '—'; const d = new Date(s); if (Number.isNaN(d.getTime())) return s; return d.toLocaleString('en-IN', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } export function dateOnly(v: unknown): string { const s = str(v); if (!s) return '—'; const d = new Date(s); if (Number.isNaN(d.getTime())) return s; return d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' }); } /** "msme" → "MSME / Business". Product codes are stored, not displayed. */ export function productLabel(v: unknown): string { const s = str(v); if (s === 'msme') return 'MSME / Business'; if (s === 'personal') return 'Personal'; return s || '—'; } export function relationshipLabel(v: unknown): string { const s = str(v); if (s === 'new_to_bank') return 'New to Bank'; if (s === 'existing') return 'Existing Customer'; return s || '—'; } /** * The assessment's reason codes arrive as one pipe-delimited string, because * that is the shape a customJS node can return without inventing a schema. * Split for display — a wall of pipes is unreadable, and each code is a * separate finding a credit manager weighs separately. */ export function reasonCodes(v: unknown): string[] { return str(v) .split('|') .map((s) => s.trim()) .filter(Boolean); }