import type { Account, AccountType, Invoice, InvoiceLine, Payslip, Voucher, } from '../types' export interface LineTotals { gross: number discount: number taxable: number tax: number net: number } /** Computes the monetary breakdown of a single invoice line. */ export function lineTotals(line: InvoiceLine): LineTotals { const gross = line.quantity * line.unitPrice const discount = Math.min(line.discount, gross) const taxable = gross - discount const tax = (taxable * line.taxRate) / 100 return { gross, discount, taxable, tax, net: taxable + tax } } export interface InvoiceTotals { gross: number discount: number taxable: number tax: number net: number } /** Aggregates all line totals of an invoice. */ export function invoiceTotals(invoice: Invoice): InvoiceTotals { return invoice.lines.reduce( (acc, line) => { const t = lineTotals(line) return { gross: acc.gross + t.gross, discount: acc.discount + t.discount, taxable: acc.taxable + t.taxable, tax: acc.tax + t.tax, net: acc.net + t.net, } }, { gross: 0, discount: 0, taxable: 0, tax: 0, net: 0 }, ) } export interface VoucherBalance { debit: number credit: number balanced: boolean } /** Sums debit/credit of a voucher and reports whether it balances. */ export function voucherBalance(voucher: Voucher): VoucherBalance { const debit = voucher.lines.reduce((sum, l) => sum + l.debit, 0) const credit = voucher.lines.reduce((sum, l) => sum + l.credit, 0) return { debit, credit, balanced: Math.abs(debit - credit) < 0.001 && debit > 0 } } export interface AccountBalance { account: Account debit: number credit: number balance: number } const isDebitNature = (type: AccountType): boolean => type === 'asset' || type === 'expense' /** * Builds a trial balance from posted vouchers and account opening balances. * Only leaf (non-group) accounts carry balances. */ export function trialBalance( accounts: Account[], vouchers: Voucher[], ): AccountBalance[] { const totals = new Map() for (const account of accounts) { if (account.isGroup) continue const opening = account.openingBalance || 0 totals.set(account.id, { debit: isDebitNature(account.type) ? Math.max(opening, 0) : Math.max(-opening, 0), credit: isDebitNature(account.type) ? Math.max(-opening, 0) : Math.max(opening, 0), }) } for (const voucher of vouchers) { if (voucher.status !== 'posted') continue for (const line of voucher.lines) { const entry = totals.get(line.accountId) if (!entry) continue entry.debit += line.debit entry.credit += line.credit } } return accounts .filter((account) => !account.isGroup) .map((account) => { const entry = totals.get(account.id) ?? { debit: 0, credit: 0 } const net = entry.debit - entry.credit const balance = isDebitNature(account.type) ? net : -net return { account, debit: entry.debit, credit: entry.credit, balance } }) } export interface LedgerRow { voucherId: string voucherNumber: number date: string description: string debit: number credit: number running: number } /** Produces the running general-ledger movements for a single account. */ export function accountLedger( accountId: string, account: Account | undefined, vouchers: Voucher[], ): LedgerRow[] { let running = account?.openingBalance ?? 0 const rows: LedgerRow[] = [] const sorted = [...vouchers] .filter((v) => v.status === 'posted') .sort((a, b) => a.date.localeCompare(b.date) || a.number - b.number) // Debit-nature accounts (asset, expense) increase with debits. // Credit-nature accounts (liability, equity, income) increase with credits. const debitNature = account ? isDebitNature(account.type) : true for (const voucher of sorted) { for (const line of voucher.lines) { if (line.accountId !== accountId) continue running += debitNature ? line.debit - line.credit : line.credit - line.debit rows.push({ voucherId: voucher.id, voucherNumber: voucher.number, date: voucher.date, description: line.description || voucher.description, debit: line.debit, credit: line.credit, running, }) } } return rows } export interface IncomeStatement { income: number expense: number profit: number } export function incomeStatement(balances: AccountBalance[]): IncomeStatement { const income = balances .filter((b) => b.account.type === 'income') .reduce((sum, b) => sum + b.balance, 0) const expense = balances .filter((b) => b.account.type === 'expense') .reduce((sum, b) => sum + b.balance, 0) return { income, expense, profit: income - expense } } export interface BalanceSheet { assets: number liabilities: number equity: number retainedEarnings: number } export function balanceSheet(balances: AccountBalance[]): BalanceSheet { const sumByType = (type: AccountType): number => balances .filter((b) => b.account.type === type) .reduce((sum, b) => sum + b.balance, 0) const income = sumByType('income') const expense = sumByType('expense') return { assets: sumByType('asset'), liabilities: sumByType('liability'), equity: sumByType('equity'), retainedEarnings: income - expense, } } /** Net pay of a payslip: earnings minus all deductions. */ export function payslipNet(payslip: Payslip): number { return ( payslip.baseSalary + payslip.overtime + payslip.bonus - payslip.deductions - payslip.insurance - payslip.tax ) }