import { suite } from '../lib/context.js' suite('Accounting', { tags: ['accounting'] }, (t) => { // ─── Invoices: CRUD ─────────────────────────────────────────────────────── t.test('creates a manual invoice', { tags: ['invoices', 'create'] }, async () => { const acct = await t.api.post('/v1/accounts', { name: 'Invoice Test Acct', billingMode: 'consolidated' }) const res = await t.api.post('/v1/invoices', { accountId: acct.data.id, issueDate: '2026-04-06', dueDate: '2026-05-06', lineItems: [ { description: 'Service A', qty: 2, unitPrice: 50, taxRate: 0.0825 }, { description: 'Service B', qty: 1, unitPrice: 100 }, ], }) t.assert.status(res, 201) t.assert.ok(res.data.invoiceNumber) t.assert.equal(res.data.status, 'draft') t.assert.ok(parseFloat(res.data.total) > 0) t.assert.equal(res.data.balance, res.data.total) }) t.test('rejects invoice without line items', { tags: ['invoices', 'validation'] }, async () => { const acct = await t.api.post('/v1/accounts', { name: 'Empty Invoice Acct', billingMode: 'consolidated' }) const res = await t.api.post('/v1/invoices', { accountId: acct.data.id, issueDate: '2026-04-06', dueDate: '2026-05-06', lineItems: [], }) t.assert.status(res, 400) }) t.test('lists invoices with pagination', { tags: ['invoices', 'list'] }, async () => { const res = await t.api.get('/v1/invoices', { page: 1, limit: 25 }) t.assert.status(res, 200) t.assert.ok(res.data.data) t.assert.ok(res.data.pagination) }) t.test('gets invoice detail with line items', { tags: ['invoices', 'detail'] }, async () => { const acct = await t.api.post('/v1/accounts', { name: 'Detail Invoice Acct', billingMode: 'consolidated' }) const inv = await t.api.post('/v1/invoices', { accountId: acct.data.id, issueDate: '2026-04-06', dueDate: '2026-05-06', lineItems: [{ description: 'Widget', qty: 3, unitPrice: 25 }], }) const res = await t.api.get(`/v1/invoices/${inv.data.id}`) t.assert.status(res, 200) t.assert.ok(res.data.lineItems) t.assert.equal(res.data.lineItems.length, 1) t.assert.ok(res.data.payments) }) t.test('returns 404 for missing invoice', { tags: ['invoices', 'detail'] }, async () => { const res = await t.api.get('/v1/invoices/00000000-0000-0000-0000-000000000000') t.assert.status(res, 404) }) // ─── Invoice Workflow ───────────────────────────────────────────────────── t.test('sends a draft invoice', { tags: ['invoices', 'workflow'] }, async () => { const acct = await t.api.post('/v1/accounts', { name: 'Send Invoice Acct', billingMode: 'consolidated' }) const inv = await t.api.post('/v1/invoices', { accountId: acct.data.id, issueDate: '2026-04-06', dueDate: '2026-05-06', lineItems: [{ description: 'Send Test', qty: 1, unitPrice: 100 }], }) const res = await t.api.post(`/v1/invoices/${inv.data.id}/send`) t.assert.status(res, 200) t.assert.equal(res.data.status, 'sent') }) t.test('applies payment to invoice', { tags: ['invoices', 'payment'] }, async () => { const acct = await t.api.post('/v1/accounts', { name: 'Payment Invoice Acct', billingMode: 'consolidated' }) const inv = await t.api.post('/v1/invoices', { accountId: acct.data.id, issueDate: '2026-04-06', dueDate: '2026-05-06', lineItems: [{ description: 'Pay Test', qty: 1, unitPrice: 200 }], }) await t.api.post(`/v1/invoices/${inv.data.id}/send`) // Partial payment const res1 = await t.api.post(`/v1/invoices/${inv.data.id}/apply-payment`, { amount: 100 }) t.assert.status(res1, 200) t.assert.ok(res1.data.id) // Check invoice updated const detail = await t.api.get(`/v1/invoices/${inv.data.id}`) t.assert.equal(detail.data.status, 'partial') t.assert.equal(detail.data.amountPaid, '100.00') t.assert.equal(detail.data.balance, '100.00') // Full payment const res2 = await t.api.post(`/v1/invoices/${inv.data.id}/apply-payment`, { amount: 100 }) t.assert.status(res2, 200) const final = await t.api.get(`/v1/invoices/${inv.data.id}`) t.assert.equal(final.data.status, 'paid') t.assert.equal(final.data.balance, '0.00') }) t.test('rejects overpayment', { tags: ['invoices', 'payment', 'validation'] }, async () => { const acct = await t.api.post('/v1/accounts', { name: 'Overpay Acct', billingMode: 'consolidated' }) const inv = await t.api.post('/v1/invoices', { accountId: acct.data.id, issueDate: '2026-04-06', dueDate: '2026-05-06', lineItems: [{ description: 'Small Item', qty: 1, unitPrice: 50 }], }) await t.api.post(`/v1/invoices/${inv.data.id}/send`) const res = await t.api.post(`/v1/invoices/${inv.data.id}/apply-payment`, { amount: 100 }) t.assert.status(res, 400) }) t.test('voids an invoice', { tags: ['invoices', 'void'] }, async () => { const acct = await t.api.post('/v1/accounts', { name: 'Void Invoice Acct', billingMode: 'consolidated' }) const inv = await t.api.post('/v1/invoices', { accountId: acct.data.id, issueDate: '2026-04-06', dueDate: '2026-05-06', lineItems: [{ description: 'Void Test', qty: 1, unitPrice: 75 }], }) await t.api.post(`/v1/invoices/${inv.data.id}/send`) const res = await t.api.post(`/v1/invoices/${inv.data.id}/void`, { reason: 'Customer changed mind' }) t.assert.status(res, 200) t.assert.equal(res.data.status, 'void') t.assert.equal(res.data.balance, '0') }) t.test('writes off a bad debt', { tags: ['invoices', 'write-off'] }, async () => { const acct = await t.api.post('/v1/accounts', { name: 'WriteOff Acct', billingMode: 'consolidated' }) const inv = await t.api.post('/v1/invoices', { accountId: acct.data.id, issueDate: '2026-04-06', dueDate: '2026-05-06', lineItems: [{ description: 'Bad Debt Item', qty: 1, unitPrice: 300 }], }) await t.api.post(`/v1/invoices/${inv.data.id}/send`) const res = await t.api.post(`/v1/invoices/${inv.data.id}/write-off`, { reason: 'Uncollectable' }) t.assert.status(res, 200) t.assert.equal(res.data.status, 'written_off') }) // ─── Account Balance ────────────────────────────────────────────────────── t.test('tracks account balance on invoice send', { tags: ['balance'] }, async () => { const acct = await t.api.post('/v1/accounts', { name: 'Balance Track Acct', billingMode: 'consolidated' }) // Create and send invoice (which triggers AR balance update via service) const inv = await t.api.post('/v1/invoices', { accountId: acct.data.id, issueDate: '2026-04-06', dueDate: '2026-05-06', lineItems: [{ description: 'Balance Test', qty: 1, unitPrice: 150 }], }) const balRes = await t.api.get(`/v1/accounts/${acct.data.id}/balance`) t.assert.status(balRes, 200) t.assert.ok(balRes.data.accountId) }) // ─── CSV Export ─────────────────────────────────────────────────────────── t.test('exports invoices as CSV', { tags: ['export'] }, async () => { const res = await t.api.get('/v1/invoices/export') t.assert.status(res, 200) // CSV response should have headers const text = typeof res.data === 'string' ? res.data : JSON.stringify(res.data) t.assert.contains(text, 'Invoice Number') }) // ─── AR Aging ───────────────────────────────────────────────────────────── t.test('returns AR aging report', { tags: ['reports'] }, async () => { const res = await t.api.get('/v1/invoices/aging') t.assert.status(res, 200) t.assert.ok(res.data.current !== undefined) t.assert.ok(res.data.days30 !== undefined) t.assert.ok(res.data.days60 !== undefined) t.assert.ok(res.data.days90plus !== undefined) t.assert.ok(res.data.total !== undefined) }) // ─── Outstanding Accounts ───────────────────────────────────────────────── t.test('lists outstanding accounts', { tags: ['balance'] }, async () => { const res = await t.api.get('/v1/accounts/outstanding', { page: 1, limit: 25 }) t.assert.status(res, 200) t.assert.ok(res.data.data) t.assert.ok(res.data.pagination) }) })