import type { DB } from './db' import { getDocument } from './repos-documents' import { getModule } from './repos-modules' import { outstandingPaise, splitProRata } from './repos-payments' import { awsCostForClient } from './repos-aws' /** Read-only reporting aggregations (D12 plain-repo pattern). Settlement per * invoice is payable − outstandingPaise (= allocations + credit-notes), clamped * to [0, payable] — identical to modulePaidView, so a paisa split here matches * a paisa split there. Nothing here mutates. */ export interface DateRange { from?: string; to?: string } /** Whole-day difference on YYYY-MM-DD (UTC), lexical-safe like scheduler.addDaysIso. */ function daysBetween(fromIso: string, toIso: string): number { return Math.floor((Date.parse(toIso) - Date.parse(fromIso)) / 86_400_000) } /** Issued, non-cancelled invoices, optionally within a doc_date range. */ function invoiceIds(db: DB, clientId: string | undefined, range?: DateRange): string[] { let sql = `SELECT id FROM document WHERE doc_type='INVOICE' AND doc_no IS NOT NULL AND status != 'cancelled'` const args: unknown[] = [] if (clientId !== undefined) { sql += ` AND client_id=?`; args.push(clientId) } if (range?.from !== undefined) { sql += ` AND doc_date >= ?`; args.push(range.from) } if (range?.to !== undefined) { sql += ` AND doc_date <= ?`; args.push(range.to) } sql += ` ORDER BY doc_date, doc_no` return (db.prepare(sql).all(...args) as { id: string }[]).map((r) => r.id) } /** Settled amount on an invoice, clamped to [0, payable]. */ function settledOf(db: DB, docId: string, payablePaise: number): number { return Math.max(0, Math.min(payablePaise, payablePaise - outstandingPaise(db, docId))) } export interface DuesAgingRow { clientId: string; clientName: string b0_30: number; b31_60: number; b61_90: number; b90p: number; totalPaise: number } export function duesAging(db: DB, today: string): DuesAgingRow[] { const invoices = db.prepare( `SELECT d.id, d.client_id, d.doc_date, c.name AS client_name FROM document d JOIN client c ON c.id = d.client_id WHERE d.doc_type='INVOICE' AND d.doc_no IS NOT NULL AND d.status NOT IN ('paid','cancelled','lost')`, ).all() as { id: string; client_id: string; doc_date: string; client_name: string }[] const acc = new Map() for (const inv of invoices) { const out = outstandingPaise(db, inv.id) if (out <= 0) continue const row = acc.get(inv.client_id) ?? { clientId: inv.client_id, clientName: inv.client_name, b0_30: 0, b31_60: 0, b61_90: 0, b90p: 0, totalPaise: 0 } const age = daysBetween(inv.doc_date, today) if (age <= 30) row.b0_30 += out else if (age <= 60) row.b31_60 += out else if (age <= 90) row.b61_90 += out else row.b90p += out row.totalPaise += out acc.set(inv.client_id, row) } return [...acc.values()].sort((a, b) => b.totalPaise - a.totalPaise) } export interface ModuleRevenueRow { moduleId: string; moduleCode: string; moduleName: string; billedPaise: number; settledPaise: number } export function moduleRevenue(db: DB, range?: DateRange): ModuleRevenueRow[] { const acc = new Map() for (const id of invoiceIds(db, undefined, range)) { const inv = getDocument(db, id)! const settledTotal = settledOf(db, inv.id, inv.payablePaise) const shares = splitProRata(settledTotal, inv.payload.lines.map((l) => l.lineTotalPaise)) inv.payload.lines.forEach((line, i) => { const e = acc.get(line.itemId) ?? { billed: 0, settled: 0 } e.billed += line.lineTotalPaise e.settled += shares[i]! acc.set(line.itemId, e) }) } return [...acc.entries()].map(([moduleId, v]) => { const mod = getModule(db, moduleId) return { moduleId, moduleCode: mod?.code ?? moduleId, moduleName: mod?.name ?? moduleId, billedPaise: v.billed, settledPaise: v.settled, } }).sort((a, b) => b.billedPaise - a.billedPaise) } export interface ProfitabilityRow { clientId: string; clientName: string billedPaise: number; settledPaise: number; awsCostPaise: number; marginPaise: number } export function clientProfitability(db: DB, range?: DateRange): ProfitabilityRow[] { const clients = db.prepare(`SELECT id, name FROM client ORDER BY name`).all() as { id: string; name: string }[] const out: ProfitabilityRow[] = [] for (const c of clients) { let billed = 0 let settled = 0 for (const id of invoiceIds(db, c.id, range)) { const inv = getDocument(db, id)! billed += inv.payablePaise settled += settledOf(db, inv.id, inv.payablePaise) } const awsCostPaise = awsCostForClient(db, c.id, range?.from, range?.to) if (billed === 0 && settled === 0 && awsCostPaise === 0) continue // omit inactive clients out.push({ clientId: c.id, clientName: c.name, billedPaise: billed, settledPaise: settled, awsCostPaise, marginPaise: settled - awsCostPaise }) } return out.sort((a, b) => b.marginPaise - a.marginPaise) }