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.
64 lines
2.9 KiB
TypeScript
64 lines
2.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { buildStatement } from '../src/statement'
|
|
import type { Doc, Payment } from '../src/api'
|
|
|
|
/** Minimal Doc factory — only the fields buildStatement reads matter. */
|
|
const doc = (over: Partial<Doc>): Doc => ({
|
|
id: 'd', docType: 'INVOICE', docNo: 'INV/1', fy: '2026-27', clientId: 'c',
|
|
docDate: '2026-04-01', status: 'sent', refDocId: null,
|
|
taxablePaise: 0, cgstPaise: 0, sgstPaise: 0, igstPaise: 0, roundOffPaise: 0, payablePaise: 0,
|
|
payload: { lines: [] }, source: 'manual', createdBy: 'u', createdAt: '', ...over,
|
|
})
|
|
const pay = (over: Partial<Payment>): Payment => ({
|
|
id: 'p', clientId: 'c', receivedOn: '2026-04-10', mode: 'bank', reference: '',
|
|
amountPaise: 0, tdsPaise: 0, createdBy: 'u', createdAt: '', ...over,
|
|
})
|
|
|
|
describe('buildStatement', () => {
|
|
it('runs a balance across invoice → payment → credit note', () => {
|
|
const s = buildStatement(
|
|
[
|
|
doc({ id: 'a', docType: 'INVOICE', docNo: 'INV/1', docDate: '2026-04-01', payablePaise: 100_00 }),
|
|
doc({ id: 'b', docType: 'CREDIT_NOTE', docNo: 'CN/1', docDate: '2026-04-20', payablePaise: 30_00 }),
|
|
],
|
|
[pay({ id: 'x', receivedOn: '2026-04-10', amountPaise: 40_00 })],
|
|
)
|
|
expect(s.rows.map((r) => r.balancePaise)).toEqual([100_00, 60_00, 30_00])
|
|
expect(s.closingPaise).toBe(30_00) // client still owes ₹30
|
|
expect(s.totalDebitPaise).toBe(100_00)
|
|
expect(s.totalCreditPaise).toBe(70_00)
|
|
})
|
|
|
|
it('excludes quotations, proformas, receipts, drafts and cancelled docs', () => {
|
|
const s = buildStatement(
|
|
[
|
|
doc({ id: 'q', docType: 'QUOTATION', docNo: 'QT/1', payablePaise: 500_00 }),
|
|
doc({ id: 'pf', docType: 'PROFORMA', docNo: 'PI/1', payablePaise: 500_00 }),
|
|
doc({ id: 'rc', docType: 'RECEIPT', docNo: 'RC/1', payablePaise: 500_00 }),
|
|
doc({ id: 'dr', docType: 'INVOICE', docNo: null, payablePaise: 500_00 }), // unissued draft
|
|
doc({ id: 'cx', docType: 'INVOICE', docNo: 'INV/9', status: 'cancelled', payablePaise: 500_00 }),
|
|
],
|
|
[],
|
|
)
|
|
expect(s.rows).toHaveLength(0)
|
|
expect(s.closingPaise).toBe(0)
|
|
})
|
|
|
|
it('orders by date, debits before credits on the same day', () => {
|
|
const s = buildStatement(
|
|
[doc({ id: 'a', docType: 'INVOICE', docNo: 'INV/1', docDate: '2026-05-05', payablePaise: 10_00 })],
|
|
[pay({ id: 'x', receivedOn: '2026-05-05', amountPaise: 4_00 })],
|
|
)
|
|
expect(s.rows.map((r) => r.kind)).toEqual(['invoice', 'payment'])
|
|
})
|
|
|
|
it('counts TDS as part of the payment credit and can close in credit (advance)', () => {
|
|
const s = buildStatement(
|
|
[doc({ id: 'a', docType: 'INVOICE', docNo: 'INV/1', docDate: '2026-06-01', payablePaise: 100_00 })],
|
|
[pay({ id: 'x', receivedOn: '2026-06-02', amountPaise: 90_00, tdsPaise: 20_00 })],
|
|
)
|
|
expect(s.rows[1]!.creditPaise).toBe(110_00) // 90 + 20 TDS
|
|
expect(s.closingPaise).toBe(-10_00) // overpaid → client in credit
|
|
})
|
|
})
|