|
|
// apps/hq/test/payments.test.ts
|
|
|
import { describe, it, expect } from 'vitest'
|
|
|
import { openDb } from '../src/db'
|
|
|
import { createClient } from '../src/repos-clients'
|
|
|
import { createModule, setPrice } from '../src/repos-modules'
|
|
|
import { createDraft, issueDocument, getDocument } from '../src/repos-documents'
|
|
|
import { recordPayment, clientLedger, modulePaidView } from '../src/repos-payments'
|
|
|
|
|
|
async function invoiceFor(db: any, clientId: string, moduleId: string) {
|
|
|
return issueDocument(db, 'u1', (await createDraft(db, 'u1', { docType: 'INVOICE', clientId,
|
|
|
lines: [{ moduleId, qty: 1, kind: 'yearly' }] })).id)
|
|
|
}
|
|
|
|
|
|
describe('payments', () => {
|
|
|
it('oldest-first default allocation; part_paid then paid; leftover is an advance', async () => {
|
|
|
const db = openDb(':memory:')
|
|
|
await db.run(`INSERT INTO setting (key, value) VALUES ('company.state_code','32')`)
|
|
|
await db.run(`INSERT INTO tax_class (class_code, rate_pct_bp, effective_from) VALUES ('GST18', 1800, '2017-07-01')`)
|
|
|
const c = await createClient(db, 'u1', { name: 'Acme', stateCode: '32' })
|
|
|
const m = await createModule(db, 'u1', { code: 'POS', name: 'POS' })
|
|
|
await setPrice(db, 'u1', { moduleId: m.id, kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-04-01' })
|
|
|
const inv1 = await invoiceFor(db, c.id, m.id) // ₹11,800
|
|
|
const inv2 = await invoiceFor(db, c.id, m.id) // ₹11,800
|
|
|
await recordPayment(db, 'u1', { clientId: c.id, receivedOn: '2026-07-10', mode: 'bank', amountPaise: 15_000_00 })
|
|
|
expect((await getDocument(db, inv1.id))!.status).toBe('paid') // 11,800 settled
|
|
|
expect((await getDocument(db, inv2.id))!.status).toBe('part_paid') // 3,200 of 11,800
|
|
|
await recordPayment(db, 'u1', { clientId: c.id, receivedOn: '2026-07-11', mode: 'upi', amountPaise: 10_000_00 })
|
|
|
expect((await getDocument(db, inv2.id))!.status).toBe('paid')
|
|
|
expect((await clientLedger(db, c.id)).advancePaise).toBe(1_400_00) // 25,000 − 23,600
|
|
|
})
|
|
|
it('invoice-minus-TDS settles in full', async () => {
|
|
|
const db = openDb(':memory:')
|
|
|
await db.run(`INSERT INTO setting (key, value) VALUES ('company.state_code','32')`)
|
|
|
await db.run(`INSERT INTO tax_class (class_code, rate_pct_bp, effective_from) VALUES ('GST18', 1800, '2017-07-01')`)
|
|
|
const c = await createClient(db, 'u1', { name: 'Acme', stateCode: '32' })
|
|
|
const m = await createModule(db, 'u1', { code: 'POS', name: 'POS' })
|
|
|
await setPrice(db, 'u1', { moduleId: m.id, kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-04-01' })
|
|
|
const inv = await invoiceFor(db, c.id, m.id) // ₹11,800; client pays minus 10% TDS on ₹10,000
|
|
|
await recordPayment(db, 'u1', { clientId: c.id, receivedOn: '2026-07-10', mode: 'bank',
|
|
|
amountPaise: 10_800_00, tdsPaise: 1_000_00 })
|
|
|
expect((await getDocument(db, inv.id))!.status).toBe('paid')
|
|
|
const view = await modulePaidView(db, c.id)
|
|
|
expect(view[0]).toMatchObject({ moduleId: m.id, billedPaise: 11_800_00, settledPaise: 11_800_00 })
|
|
|
})
|
|
|
})
|