You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
79 lines
2.9 KiB
TypeScript
79 lines
2.9 KiB
TypeScript
import type { Doc, Payment } from './api'
|
|
|
|
/**
|
|
* One line of a client account statement: an invoice (debit), a credit note (credit)
|
|
* or a payment (credit), with the running balance AFTER the line is applied. Kept as a
|
|
* pure data shape so the running-balance maths can be unit-tested without the DOM.
|
|
*/
|
|
export interface StatementRow {
|
|
date: string
|
|
kind: 'invoice' | 'credit_note' | 'payment'
|
|
particulars: string
|
|
ref: string
|
|
/** Charge that raises what the client owes (0 when this row is a credit). */
|
|
debitPaise: number
|
|
/** Credit that lowers what the client owes (0 when this row is a debit). */
|
|
creditPaise: number
|
|
/** Cumulative balance after this row — positive = owed by client, negative = in credit. */
|
|
balancePaise: number
|
|
}
|
|
|
|
export interface Statement {
|
|
rows: StatementRow[]
|
|
totalDebitPaise: number
|
|
totalCreditPaise: number
|
|
/** Final running balance: > 0 the client owes us, < 0 they are in credit (advance). */
|
|
closingPaise: number
|
|
}
|
|
|
|
/**
|
|
* Build a statement of account from the client's ledger. Only posted receivables move
|
|
* the balance: issued (numbered), non-cancelled INVOICEs are debits and CREDIT_NOTEs are
|
|
* credits; every payment is a credit (amount + TDS, matching how the ledger settles).
|
|
* Quotations, proformas, receipts and drafts are not account postings and are excluded.
|
|
* Rows are ordered by date, debits before credits on the same day (conventional).
|
|
*/
|
|
export function buildStatement(documents: Doc[], payments: Payment[]): Statement {
|
|
type Entry = { date: string; order: number; row: Omit<StatementRow, 'balancePaise'> }
|
|
const entries: Entry[] = []
|
|
|
|
for (const d of documents) {
|
|
if (d.docNo === null || d.status === 'cancelled') continue
|
|
if (d.docType === 'INVOICE') {
|
|
entries.push({
|
|
date: d.docDate, order: 0,
|
|
row: { date: d.docDate, kind: 'invoice', particulars: 'Invoice', ref: d.docNo, debitPaise: d.payablePaise, creditPaise: 0 },
|
|
})
|
|
} else if (d.docType === 'CREDIT_NOTE') {
|
|
entries.push({
|
|
date: d.docDate, order: 0,
|
|
row: { date: d.docDate, kind: 'credit_note', particulars: 'Credit note', ref: d.docNo, debitPaise: 0, creditPaise: d.payablePaise },
|
|
})
|
|
}
|
|
}
|
|
for (const p of payments) {
|
|
entries.push({
|
|
date: p.receivedOn, order: 1,
|
|
row: {
|
|
date: p.receivedOn, kind: 'payment',
|
|
particulars: `Payment (${p.mode})`, ref: p.reference,
|
|
debitPaise: 0, creditPaise: p.amountPaise + p.tdsPaise,
|
|
},
|
|
})
|
|
}
|
|
|
|
entries.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : a.order - b.order))
|
|
|
|
let balance = 0
|
|
let totalDebitPaise = 0
|
|
let totalCreditPaise = 0
|
|
const rows: StatementRow[] = entries.map((e) => {
|
|
balance += e.row.debitPaise - e.row.creditPaise
|
|
totalDebitPaise += e.row.debitPaise
|
|
totalCreditPaise += e.row.creditPaise
|
|
return { ...e.row, balancePaise: balance }
|
|
})
|
|
|
|
return { rows, totalDebitPaise, totalCreditPaise, closingPaise: balance }
|
|
}
|