import { suite } from '../lib/context.js' const LOCATION_ID = '10000000-1000-4000-8000-000000000002' const ROUNDING_LOCATION_ID = '10000000-1000-4000-8000-000000000003' suite('POS', { tags: ['pos'] }, (t) => { // ─── Discounts (CRUD) ────────────────────────────────────────────────────── t.test('creates a discount', { tags: ['discounts', 'create'] }, async () => { const res = await t.api.post('/v1/discounts', { name: 'Employee 10%', discountType: 'percent', discountValue: 10, appliesTo: 'line_item', }) t.assert.status(res, 201) t.assert.equal(res.data.name, 'Employee 10%') t.assert.equal(res.data.discountType, 'percent') t.assert.ok(res.data.id) }) t.test('rejects discount without name', { tags: ['discounts', 'validation'] }, async () => { const res = await t.api.post('/v1/discounts', { discountType: 'fixed', discountValue: 5 }) t.assert.status(res, 400) }) t.test('lists discounts with pagination', { tags: ['discounts', 'list'] }, async () => { await t.api.post('/v1/discounts', { name: 'Disc A', discountType: 'fixed', discountValue: 5 }) await t.api.post('/v1/discounts', { name: 'Disc B', discountType: 'percent', discountValue: 15 }) const res = await t.api.get('/v1/discounts', { page: 1, limit: 25 }) t.assert.status(res, 200) t.assert.ok(res.data.data.length >= 2) t.assert.ok(res.data.pagination) }) t.test('lists all discounts (lookup)', { tags: ['discounts', 'list'] }, async () => { const res = await t.api.get('/v1/discounts/all') t.assert.status(res, 200) t.assert.ok(Array.isArray(res.data)) }) t.test('gets discount by id', { tags: ['discounts', 'read'] }, async () => { const created = await t.api.post('/v1/discounts', { name: 'Get Me', discountType: 'fixed', discountValue: 3 }) const res = await t.api.get(`/v1/discounts/${created.data.id}`) t.assert.status(res, 200) t.assert.equal(res.data.name, 'Get Me') }) t.test('updates a discount', { tags: ['discounts', 'update'] }, async () => { const created = await t.api.post('/v1/discounts', { name: 'Before', discountType: 'fixed', discountValue: 1 }) const res = await t.api.patch(`/v1/discounts/${created.data.id}`, { name: 'After', discountValue: 99 }) t.assert.status(res, 200) t.assert.equal(res.data.name, 'After') }) t.test('soft-deletes a discount', { tags: ['discounts', 'delete'] }, async () => { const created = await t.api.post('/v1/discounts', { name: 'Delete Me', discountType: 'fixed', discountValue: 1 }) const res = await t.api.del(`/v1/discounts/${created.data.id}`) t.assert.status(res, 200) t.assert.equal(res.data.isActive, false) }) // ─── Drawer Sessions ─────────────────────────────────────────────────────── t.test('opens a drawer session', { tags: ['drawer', 'create'] }, async () => { const res = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 200 }) t.assert.status(res, 201) t.assert.equal(res.data.status, 'open') t.assert.ok(res.data.id) // Close it so future tests can open a new one await t.api.post(`/v1/drawer/${res.data.id}/close`, { closingBalance: 200 }) }) t.test('rejects opening second drawer at same location', { tags: ['drawer', 'validation'] }, async () => { const first = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 100 }) t.assert.status(first, 201) const second = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 100 }) t.assert.status(second, 409) // Cleanup await t.api.post(`/v1/drawer/${first.data.id}/close`, { closingBalance: 100 }) }) t.test('closes a drawer session with denominations', { tags: ['drawer', 'close'] }, async () => { const opened = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 150 }) t.assert.status(opened, 201) const res = await t.api.post(`/v1/drawer/${opened.data.id}/close`, { closingBalance: 155, denominations: { ones: 50, fives: 20, tens: 5, twenties: 2 }, notes: 'End of shift', }) t.assert.status(res, 200) t.assert.equal(res.data.status, 'closed') t.assert.ok(res.data.closedAt) }) t.test('gets current open drawer for location', { tags: ['drawer', 'read'] }, async () => { const opened = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 100 }) t.assert.status(opened, 201) const res = await t.api.get('/v1/drawer/current', { locationId: LOCATION_ID }) t.assert.status(res, 200) t.assert.equal(res.data.id, opened.data.id) // Cleanup await t.api.post(`/v1/drawer/${opened.data.id}/close`, { closingBalance: 100 }) }) t.test('lists drawer sessions with pagination', { tags: ['drawer', 'list'] }, async () => { const res = await t.api.get('/v1/drawer', { page: 1, limit: 25 }) t.assert.status(res, 200) 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 () => { const res = await t.api.post('/v1/transactions', { transactionType: 'sale', locationId: LOCATION_ID, }) t.assert.status(res, 201) t.assert.equal(res.data.transactionType, 'sale') t.assert.equal(res.data.status, 'pending') t.assert.ok(res.data.transactionNumber) t.assert.contains(res.data.transactionNumber, 'TXN-') }) t.test('rejects transaction without type', { tags: ['transactions', 'validation'] }, async () => { const res = await t.api.post('/v1/transactions', { locationId: LOCATION_ID }) t.assert.status(res, 400) }) t.test('adds line items and calculates tax', { tags: ['transactions', 'line-items', 'tax'] }, async () => { const txn = await t.api.post('/v1/transactions', { transactionType: 'sale', locationId: LOCATION_ID, }) t.assert.status(txn, 201) // Add a line item (no product — ad hoc, taxed as goods at 8.25%) const item = await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, { description: 'Violin Strings', qty: 2, unitPrice: 12.99, }) t.assert.status(item, 201) t.assert.equal(item.data.description, 'Violin Strings') t.assert.equal(item.data.qty, 2) // Verify tax was applied (8.25% on $25.98 = ~$2.14) const taxAmount = parseFloat(item.data.taxAmount) t.assert.greaterThan(taxAmount, 0) // Verify transaction totals were recalculated const updated = await t.api.get(`/v1/transactions/${txn.data.id}`) t.assert.status(updated, 200) const total = parseFloat(updated.data.total) t.assert.greaterThan(total, 0) }) t.test('removes a line item and recalculates', { tags: ['transactions', 'line-items'] }, async () => { const txn = await t.api.post('/v1/transactions', { transactionType: 'sale', locationId: LOCATION_ID }) const item1 = await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, { description: 'Item A', qty: 1, unitPrice: 10, }) await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, { description: 'Item B', qty: 1, unitPrice: 20, }) // Remove item A const del = await t.api.del(`/v1/transactions/${txn.data.id}/line-items/${item1.data.id}`) t.assert.status(del, 200) // Transaction should only have item B's total const updated = await t.api.get(`/v1/transactions/${txn.data.id}`) const subtotal = parseFloat(updated.data.subtotal) t.assert.equal(subtotal, 20) }) t.test('tax exempt transaction has zero tax', { tags: ['transactions', 'tax'] }, async () => { const txn = await t.api.post('/v1/transactions', { transactionType: 'sale', locationId: LOCATION_ID, taxExempt: true, taxExemptReason: 'Non-profit org', }) t.assert.status(txn, 201) const item = await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, { description: 'Exempt Item', qty: 1, unitPrice: 100, }) t.assert.status(item, 201) t.assert.equal(parseFloat(item.data.taxAmount), 0) const updated = await t.api.get(`/v1/transactions/${txn.data.id}`) t.assert.equal(parseFloat(updated.data.taxTotal), 0) t.assert.equal(parseFloat(updated.data.total), 100) }) t.test('transaction without location has zero tax', { tags: ['transactions', 'tax'] }, async () => { const txn = await t.api.post('/v1/transactions', { transactionType: 'sale' }) t.assert.status(txn, 201) const item = await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, { description: 'No-location Item', qty: 1, unitPrice: 50, }) t.assert.status(item, 201) t.assert.equal(parseFloat(item.data.taxAmount), 0) }) // ─── Discounts on Transactions ───────────────────────────────────────────── t.test('applies a line-item discount', { tags: ['transactions', 'discounts'] }, async () => { const txn = await t.api.post('/v1/transactions', { transactionType: 'sale', locationId: LOCATION_ID }) const item = await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, { description: 'Discountable Item', qty: 1, unitPrice: 100, }) const res = await t.api.post(`/v1/transactions/${txn.data.id}/discounts`, { amount: 15, reason: 'Loyalty discount', lineItemId: item.data.id, }) t.assert.status(res, 200) // Verify discount reduced the total const updated = await t.api.get(`/v1/transactions/${txn.data.id}`) const discountTotal = parseFloat(updated.data.discountTotal) t.assert.equal(discountTotal, 15) // Total should be (100 - 15) + tax on 85 const total = parseFloat(updated.data.total) t.assert.greaterThan(total, 80) }) t.test('rejects discount exceeding approval threshold', { tags: ['transactions', 'discounts', 'validation'] }, async () => { // Create a discount with approval threshold const disc = await t.api.post('/v1/discounts', { name: 'Threshold Disc', discountType: 'fixed', discountValue: 50, requiresApprovalAbove: 20, }) const txn = await t.api.post('/v1/transactions', { transactionType: 'sale', locationId: LOCATION_ID }) const item = await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, { description: 'Expensive Item', qty: 1, unitPrice: 200, }) // Try to apply $25 discount (above $20 threshold) const res = await t.api.post(`/v1/transactions/${txn.data.id}/discounts`, { discountId: disc.data.id, amount: 25, reason: 'Trying too much', lineItemId: item.data.id, }) t.assert.status(res, 400) }) // ─── 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`, { description: 'Cash Sale Item', qty: 1, unitPrice: 10, }) // Get updated total const pending = await t.api.get(`/v1/transactions/${txn.data.id}`) const total = parseFloat(pending.data.total) const res = await t.api.post(`/v1/transactions/${txn.data.id}/complete`, { paymentMethod: 'cash', amountTendered: 20, }) t.assert.status(res, 200) t.assert.equal(res.data.status, 'completed') t.assert.equal(res.data.paymentMethod, 'cash') const changeGiven = parseFloat(res.data.changeGiven) // change = 20 - total const expectedChange = 20 - total t.assert.equal(changeGiven, expectedChange) }) t.test('rejects cash payment with insufficient amount', { tags: ['transactions', 'complete', 'validation'] }, 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`, { description: 'Underpay Item', qty: 1, unitPrice: 100, }) const res = await t.api.post(`/v1/transactions/${txn.data.id}/complete`, { paymentMethod: 'cash', amountTendered: 5, }) t.assert.status(res, 400) }) t.test('completes a card transaction', { tags: ['transactions', 'complete'] }, 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`, { description: 'Card Sale Item', qty: 1, unitPrice: 49.99, }) const res = await t.api.post(`/v1/transactions/${txn.data.id}/complete`, { paymentMethod: 'card_present', }) t.assert.status(res, 200) t.assert.equal(res.data.status, 'completed') t.assert.equal(res.data.paymentMethod, 'card_present') }) t.test('rejects completing a non-pending transaction', { tags: ['transactions', 'complete', 'validation'] }, 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`, { description: 'Double Complete', qty: 1, unitPrice: 10, }) await t.api.post(`/v1/transactions/${txn.data.id}/complete`, { paymentMethod: 'card_present', }) // Try completing again const res = await t.api.post(`/v1/transactions/${txn.data.id}/complete`, { paymentMethod: 'cash', amountTendered: 100, }) t.assert.status(res, 409) }) // ─── Void Transaction ────────────────────────────────────────────────────── t.test('voids a pending transaction', { tags: ['transactions', 'void'] }, 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`, { description: 'Void Me', qty: 1, unitPrice: 25, }) const res = await t.api.post(`/v1/transactions/${txn.data.id}/void`) t.assert.status(res, 200) t.assert.equal(res.data.status, 'voided') }) t.test('rejects voiding a completed transaction', { tags: ['transactions', 'void', 'validation'] }, 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`, { description: 'No Void', qty: 1, unitPrice: 10, }) await t.api.post(`/v1/transactions/${txn.data.id}/complete`, { paymentMethod: 'card_present' }) const res = await t.api.post(`/v1/transactions/${txn.data.id}/void`) t.assert.status(res, 409) }) // ─── Receipt ─────────────────────────────────────────────────────────────── t.test('gets transaction receipt', { tags: ['transactions', 'receipt'] }, 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`, { description: 'Receipt Item', qty: 1, unitPrice: 42, }) await t.api.post(`/v1/transactions/${txn.data.id}/complete`, { paymentMethod: 'card_present' }) const res = await t.api.get(`/v1/transactions/${txn.data.id}/receipt`) t.assert.status(res, 200) t.assert.ok(res.data.transaction) t.assert.ok(res.data.company) t.assert.ok(res.data.location) t.assert.equal(res.data.transaction.transactionNumber.startsWith('TXN-'), true) }) // ─── List Transactions ───────────────────────────────────────────────────── t.test('lists transactions with pagination', { tags: ['transactions', 'list'] }, async () => { const res = await t.api.get('/v1/transactions', { page: 1, limit: 25 }) t.assert.status(res, 200) t.assert.ok(res.data.data.length >= 1) t.assert.ok(res.data.pagination) }) t.test('filters transactions by status', { tags: ['transactions', 'list', 'filter'] }, async () => { const res = await t.api.get('/v1/transactions', { status: 'completed' }) t.assert.status(res, 200) for (const txn of res.data.data) { t.assert.equal(txn.status, 'completed') } }) // ─── Tax Lookup (stub) ──────────────────────────────────────────────────── t.test('tax lookup returns 501 (not configured)', { tags: ['tax'] }, async () => { const res = await t.api.get('/v1/tax/lookup/90210') t.assert.status(res, 501) }) t.test('rejects invalid zip format', { tags: ['tax', 'validation'] }, async () => { const res = await t.api.get('/v1/tax/lookup/abc') t.assert.status(res, 400) }) // ─── 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', { transactionType: 'sale', locationId: ROUNDING_LOCATION_ID, }) t.assert.status(txn, 201) // Add item that will produce a total not divisible by $0.05 // $10.01 + 8.25% tax = $10.01 + $0.83 = $10.84 → rounds to $10.85 await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, { description: 'Rounding Test Item', qty: 1, unitPrice: 10.01, }) const pending = await t.api.get(`/v1/transactions/${txn.data.id}`) const exactTotal = parseFloat(pending.data.total) // Complete with cash — should round const res = await t.api.post(`/v1/transactions/${txn.data.id}/complete`, { paymentMethod: 'cash', amountTendered: 20, }) t.assert.status(res, 200) const roundingAdj = parseFloat(res.data.roundingAdjustment) const changeGiven = parseFloat(res.data.changeGiven) const roundedTotal = exactTotal + roundingAdj // Rounded total should be divisible by 0.05 t.assert.equal(Math.round(roundedTotal * 100) % 5, 0) // Change should be based on rounded total t.assert.equal(changeGiven, Math.round((20 - roundedTotal) * 100) / 100) // Adjustment should be small (-0.02 to +0.02) t.assert.ok(Math.abs(roundingAdj) <= 0.02) }) t.test('card payment skips rounding even at rounding location', { tags: ['transactions', 'rounding'] }, async () => { const txn = await t.api.post('/v1/transactions', { transactionType: 'sale', locationId: ROUNDING_LOCATION_ID, }) await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, { description: 'Card Rounding Test', qty: 1, unitPrice: 10.01, }) const res = await t.api.post(`/v1/transactions/${txn.data.id}/complete`, { paymentMethod: 'card_present', }) t.assert.status(res, 200) t.assert.equal(parseFloat(res.data.roundingAdjustment), 0) }) 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, }) await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, { description: 'No Rounding Item', qty: 1, unitPrice: 10.01, }) const res = await t.api.post(`/v1/transactions/${txn.data.id}/complete`, { paymentMethod: 'cash', amountTendered: 20, }) 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 ──────────────────────────────────────────────────────── t.test('full sale flow: open drawer, sell, close drawer', { tags: ['e2e'] }, async () => { // 1. Open drawer const drawer = await t.api.post('/v1/drawer/open', { locationId: LOCATION_ID, openingBalance: 100 }) t.assert.status(drawer, 201) // 2. Create transaction const txn = await t.api.post('/v1/transactions', { transactionType: 'sale', locationId: LOCATION_ID, }) t.assert.status(txn, 201) // 3. Add line items await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, { description: 'Guitar Pick (12pk)', qty: 3, unitPrice: 5.99, }) await t.api.post(`/v1/transactions/${txn.data.id}/line-items`, { description: 'Capo', qty: 1, unitPrice: 19.99, }) // 4. Verify totals const pending = await t.api.get(`/v1/transactions/${txn.data.id}`) t.assert.status(pending, 200) const subtotal = parseFloat(pending.data.subtotal) const taxTotal = parseFloat(pending.data.taxTotal) const total = parseFloat(pending.data.total) // subtotal = 3*5.99 + 19.99 = 37.96 t.assert.equal(subtotal, 37.96) t.assert.greaterThan(taxTotal, 0) t.assert.equal(total, subtotal + taxTotal) t.assert.equal(pending.data.lineItems.length, 2) // 5. Complete with cash const completed = await t.api.post(`/v1/transactions/${txn.data.id}/complete`, { paymentMethod: 'cash', amountTendered: 50, }) t.assert.status(completed, 200) t.assert.equal(completed.data.status, 'completed') const change = parseFloat(completed.data.changeGiven) t.assert.equal(change, Math.round((50 - total) * 100) / 100) // 6. Get receipt const receipt = await t.api.get(`/v1/transactions/${txn.data.id}/receipt`) t.assert.status(receipt, 200) t.assert.equal(receipt.data.transaction.status, 'completed') // 7. Close drawer const closed = await t.api.post(`/v1/drawer/${drawer.data.id}/close`, { closingBalance: 100 + total - change, notes: 'End of day', }) t.assert.status(closed, 200) t.assert.equal(closed.data.status, 'closed') }) })