fix: require open drawer to complete transactions, fix product price field
Some checks failed
CI / ci (pull_request) Failing after 20s
CI / e2e (pull_request) Has been skipped

- Backend enforces open drawer at location before completing any transaction
- Frontend disables payment buttons when drawer is closed with warning message
- Fix product price field name (price, not sellingPrice) in POS API types
- Fix seed UUIDs to use valid UUID v4 format (version nibble must be 1-8)
- Fix Vite allowedHosts for dev.lunarfront.tech access
- Add e2e test for drawer enforcement (39 POS tests now pass)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ryan
2026-04-04 19:54:07 +00:00
parent bd3a25aa1c
commit 1673e18fe8
8 changed files with 125 additions and 44 deletions

View File

@@ -273,6 +273,36 @@ suite('POS', { tags: ['pos'] }, (t) => {
// ─── Complete Transaction ──────────────────────────────────────────────────
t.test('rejects completing transaction without open drawer', { tags: ['transactions', 'complete', 'validation', 'drawer'] }, async () => {
// Ensure no drawer is open at LOCATION_ID
const current = await t.api.get('/v1/drawer/current', { locationId: LOCATION_ID })
if (current.status === 200 && current.data.id) {
await t.api.post(`/v1/drawer/${current.data.id}/close`, { closingBalance: 0 })
}
const txn = await t.api.post('/v1/transactions', { transactionType: 'sale', locationId: LOCATION_ID })
await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, {
description: 'No Drawer Item',
qty: 1,
unitPrice: 10,
})
const res = await t.api.post(`/v1/transactions/${txn.data.id}/complete`, {
paymentMethod: 'cash',
amountTendered: 20,
})
t.assert.status(res, 400)
// Void to clean up
await t.api.post(`/v1/transactions/${txn.data.id}/void`)
})
// Open a drawer for the remaining complete tests
t.test('opens drawer for complete tests', { tags: ['transactions', 'complete', 'setup'] }, async () => {
const res = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 200 })
t.assert.status(res, 201)
})
t.test('completes a cash transaction with change', { tags: ['transactions', 'complete', 'cash'] }, async () => {
const txn = await t.api.post('/v1/transactions', { transactionType: 'sale', locationId: LOCATION_ID })
await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, {
@@ -427,6 +457,18 @@ suite('POS', { tags: ['pos'] }, (t) => {
// ─── Cash Rounding ─────────────────────────────────────────────────────────
// Close the LOCATION_ID drawer and open one at ROUNDING_LOCATION_ID
t.test('setup drawer for rounding tests', { tags: ['transactions', 'rounding', 'setup'] }, async () => {
// Close drawer at LOCATION_ID
const current = await t.api.get('/v1/drawer/current', { locationId: LOCATION_ID })
if (current.status === 200 && current.data.id) {
await t.api.post(`/v1/drawer/${current.data.id}/close`, { closingBalance: 200 })
}
// Open drawer at ROUNDING_LOCATION_ID
const res = await t.api.post('/v1/drawer/open', { locationId: ROUNDING_LOCATION_ID, openingBalance: 200 })
t.assert.status(res, 201)
})
t.test('cash rounding adjusts total to nearest nickel', { tags: ['transactions', 'rounding'] }, async () => {
// Create transaction at the rounding-enabled location
const txn = await t.api.post('/v1/transactions', {
@@ -484,6 +526,10 @@ suite('POS', { tags: ['pos'] }, (t) => {
})
t.test('no rounding at non-rounding location', { tags: ['transactions', 'rounding'] }, async () => {
// Open drawer at LOCATION_ID for this test
const drawer = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 100 })
t.assert.status(drawer, 201)
const txn = await t.api.post('/v1/transactions', {
transactionType: 'sale',
locationId: LOCATION_ID,
@@ -500,6 +546,17 @@ suite('POS', { tags: ['pos'] }, (t) => {
})
t.assert.status(res, 200)
t.assert.equal(parseFloat(res.data.roundingAdjustment), 0)
// Cleanup
await t.api.post(`/v1/drawer/${drawer.data.id}/close`, { closingBalance: 100 })
})
// Close rounding location drawer
t.test('cleanup rounding drawer', { tags: ['transactions', 'rounding', 'setup'] }, async () => {
const current = await t.api.get('/v1/drawer/current', { locationId: ROUNDING_LOCATION_ID })
if (current.status === 200 && current.data.id) {
await t.api.post(`/v1/drawer/${current.data.id}/close`, { closingBalance: 200 })
}
})
// ─── Full POS Flow ────────────────────────────────────────────────────────