import type { DB } from './db' import { createDraft, type Doc } from './repos-documents' import { getClientModule, priceOn, type Kind } from './repos-modules' /** * Renewals command center (D27). One unified list of what's coming up for renewal — * module subscriptions (client_module.next_renewal, priced by the dated price book) and * AMC contracts (amc_contract.period_to, its fixed amount) — with days-until and amount, * so recurring revenue is a worklist, not a memory test. Read-only. The renewal REMINDERS * are already scanned by the scheduler (rule kinds renewal_due / amc_expiring); this is the * human-facing view + the one-click "raise the renewal paper" action. */ function daysBetween(fromIso: string, toIso: string): number { return Math.floor((Date.parse(toIso) - Date.parse(fromIso)) / 86_400_000) } export interface RenewalRow { kind: 'module' | 'amc' id: string // client_module id (module) or amc_contract id (amc) clientId: string; clientName: string label: string // module code, or 'AMC — ' dueOn: string // YYYY-MM-DD daysUntil: number // negative = already lapsed amountPaise: number | null // renewal value (price book / AMC amount); null if no price row } export async function renewalsDue(db: DB, from: string, to: string, today: string): Promise { const rows: RenewalRow[] = [] const mods = await db.all<{ id: string; client_id: string; module_id: string; kind: string; edition: string next_renewal: string; client_name: string; code: string }>( `SELECT cm.id, cm.client_id, cm.module_id, cm.kind, cm.edition, cm.next_renewal, c.name AS client_name, m.code FROM client_module cm JOIN client c ON c.id = cm.client_id JOIN module m ON m.id = cm.module_id WHERE cm.active=1 AND cm.next_renewal IS NOT NULL AND cm.next_renewal >= ? AND cm.next_renewal <= ?`, from, to, ) for (const r of mods) { rows.push({ kind: 'module', id: r.id, clientId: r.client_id, clientName: r.client_name, label: r.code, dueOn: r.next_renewal, daysUntil: daysBetween(today, r.next_renewal), amountPaise: await priceOn(db, r.module_id, r.kind as Kind, r.edition, r.next_renewal), }) } const amcs = await db.all<{ id: string; client_id: string; coverage: string; period_to: string; amount_paise: number; client_name: string }>( `SELECT a.id, a.client_id, a.coverage, a.period_to, a.amount_paise, c.name AS client_name FROM amc_contract a JOIN client c ON c.id = a.client_id WHERE a.active=1 AND a.period_to >= ? AND a.period_to <= ?`, from, to, ) for (const r of amcs) { rows.push({ kind: 'amc', id: r.id, clientId: r.client_id, clientName: r.client_name, label: `AMC — ${r.coverage !== '' ? r.coverage : 'contract'}`, dueOn: r.period_to, daysUntil: daysBetween(today, r.period_to), amountPaise: r.amount_paise, }) } return rows.sort((a, b) => a.dueOn.localeCompare(b.dueOn) || a.clientName.localeCompare(b.clientName)) } /** * One-click renewal PROFORMA for a module subscription — carries the client's kind/edition * forward into a fresh proforma draft, priced by the dated price book (via createDraft). * The staffer reviews + issues + sends it like any other document. (AMC renewals use the * existing generateAmcRenewalInvoice.) */ export async function generateModuleRenewalQuote(db: DB, userId: string, clientModuleId: string): Promise { const cm = await getClientModule(db, clientModuleId) if (cm === null) throw new Error('Client module not found') return createDraft(db, userId, { docType: 'PROFORMA', clientId: cm.clientId, source: 'module_renewal', lines: [{ moduleId: cm.moduleId, qty: 1, kind: cm.kind, edition: cm.edition }], }) } /** * Ad-hoc bulk SMS credit purchase (top-up) — a PROFORMA priced through the same * createDraft path as any other document, so the usage rate card (D23) resolves the * per-SMS rate for `smsQty` the same way a subscription line would. Tagged * `source: 'bulk_sms_topup'` so it reads distinctly from a subscription renewal * (`module_renewal`) on the client-detail document list. */ export async function generateBulkSmsPurchaseQuote( db: DB, userId: string, clientModuleId: string, smsQty: number, ): Promise { if (!Number.isInteger(smsQty) || smsQty <= 0) throw new Error('SMS quantity must be a positive integer') const cm = await getClientModule(db, clientModuleId) if (cm === null) throw new Error('Client module not found') return createDraft(db, userId, { docType: 'PROFORMA', clientId: cm.clientId, source: 'bulk_sms_topup', lines: [{ moduleId: cm.moduleId, qty: smsQty, kind: cm.kind, edition: cm.edition }], }) }