feat: add drawer cash in/out adjustments with balance reconciliation

- New drawer_adjustment table (type: cash_in/cash_out, amount, reason)
- POST/GET /drawer/:id/adjustments endpoints
- Drawer close calculation now includes adjustments: expected = opening + sales + cash_in - cash_out
- DrawerAdjustmentSchema for input validation
- 5 new tests (44 total POS tests passing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ryan
2026-04-04 20:24:55 +00:00
parent 2b9e99bbd6
commit c66554f932
8 changed files with 180 additions and 6 deletions

View File

@@ -114,6 +114,81 @@ suite('POS', { tags: ['pos'] }, (t) => {
t.assert.ok(res.data.pagination)
})
// ─── Drawer Adjustments ─────────────────────────────────────────────────────
t.test('adds cash out adjustment to open drawer', { tags: ['drawer', 'adjustments'] }, async () => {
const drawer = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 200 })
t.assert.status(drawer, 201)
const res = await t.api.post(`/v1/drawer/${drawer.data.id}/adjustments`, {
type: 'cash_out',
amount: 50,
reason: 'Bank deposit',
})
t.assert.status(res, 201)
t.assert.equal(res.data.type, 'cash_out')
t.assert.equal(parseFloat(res.data.amount), 50)
// Cleanup
await t.api.post(`/v1/drawer/${drawer.data.id}/close`, { closingBalance: 150 })
})
t.test('adds cash in adjustment to open drawer', { tags: ['drawer', 'adjustments'] }, async () => {
const drawer = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 100 })
t.assert.status(drawer, 201)
const res = await t.api.post(`/v1/drawer/${drawer.data.id}/adjustments`, {
type: 'cash_in',
amount: 25,
reason: 'Change from petty cash',
})
t.assert.status(res, 201)
t.assert.equal(res.data.type, 'cash_in')
// Cleanup
await t.api.post(`/v1/drawer/${drawer.data.id}/close`, { closingBalance: 125 })
})
t.test('lists drawer adjustments', { tags: ['drawer', 'adjustments'] }, async () => {
const drawer = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 100 })
await t.api.post(`/v1/drawer/${drawer.data.id}/adjustments`, { type: 'cash_out', amount: 30, reason: 'Test out' })
await t.api.post(`/v1/drawer/${drawer.data.id}/adjustments`, { type: 'cash_in', amount: 10, reason: 'Test in' })
const res = await t.api.get(`/v1/drawer/${drawer.data.id}/adjustments`)
t.assert.status(res, 200)
t.assert.equal(res.data.data.length, 2)
// Cleanup
await t.api.post(`/v1/drawer/${drawer.data.id}/close`, { closingBalance: 80 })
})
t.test('drawer close includes adjustments in expected balance', { tags: ['drawer', 'adjustments', 'close'] }, async () => {
const drawer = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 200 })
t.assert.status(drawer, 201)
// Cash out $50, cash in $20 → net adjustment = -$30
await t.api.post(`/v1/drawer/${drawer.data.id}/adjustments`, { type: 'cash_out', amount: 50, reason: 'Bank drop' })
await t.api.post(`/v1/drawer/${drawer.data.id}/adjustments`, { type: 'cash_in', amount: 20, reason: 'Extra change' })
// Close — expected = 200 (opening) + 0 (no sales) + 20 (in) - 50 (out) = 170
const closed = await t.api.post(`/v1/drawer/${drawer.data.id}/close`, { closingBalance: 170 })
t.assert.status(closed, 200)
t.assert.equal(parseFloat(closed.data.expectedBalance), 170)
t.assert.equal(parseFloat(closed.data.overShort), 0)
})
t.test('rejects adjustment on closed drawer', { tags: ['drawer', 'adjustments', 'validation'] }, async () => {
const drawer = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 100 })
await t.api.post(`/v1/drawer/${drawer.data.id}/close`, { closingBalance: 100 })
const res = await t.api.post(`/v1/drawer/${drawer.data.id}/adjustments`, {
type: 'cash_out',
amount: 10,
reason: 'Should fail',
})
t.assert.status(res, 409)
})
// ─── Transactions ──────────────────────────────────────────────────────────
t.test('creates a sale transaction', { tags: ['transactions', 'create'] }, async () => {