feat: add feature
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-10-19 23:23:36 +02:00
parent fa38ac7617
commit 874f68b4b1
11 changed files with 577 additions and 2 deletions
+106
View File
@@ -0,0 +1,106 @@
import { Prisma } from '@prisma/client';
import { parseTabEvents, tabEventRow } from './tab-events';
const now = new Date('2026-10-24T22:00:00.000Z');
const lineAdded = {
id: '6f1c2c0e-6a2b-4c1e-9a4c-1f2a3b4c5d6e',
type: 'line_added',
tabId: 'tab-1',
customerName: 'Jan',
occurredAt: '2026-10-24T21:41:03.120Z',
productName: 'Cola',
unitPriceInCents: 250,
quantity: 2,
};
const settled = {
id: '7a2d3e4f-5b6c-4d7e-8f9a-0b1c2d3e4f5a',
type: 'settled',
tabId: 'tab-1',
customerName: 'Jan',
occurredAt: '2026-10-24T21:55:00.000Z',
amountInCents: 500,
paymentMethod: 'payconiq',
items: [{ productName: 'Cola', quantity: 2, unitPriceInCents: 250 }],
};
describe('parseTabEvents', () => {
it('accepts valid line and settlement events', () => {
const { accepted, rejected } = parseTabEvents([lineAdded, settled]);
expect(accepted.map((e) => e.id)).toEqual([lineAdded.id, settled.id]);
expect(rejected).toEqual([]);
});
it('rejects invalid events one by one, keeping their id', () => {
const { accepted, rejected } = parseTabEvents([
lineAdded,
{ ...lineAdded, id: '8b3e4f5a-6c7d-4e8f-9a0b-1c2d3e4f5a6b', quantity: 0 },
{ ...settled, id: '9c4f5a6b-7d8e-4f9a-0b1c-2d3e4f5a6b7c', paymentMethod: 'bitcoin' },
{ ...lineAdded, id: 'not-a-uuid' },
'nonsense',
]);
expect(accepted.map((e) => e.id)).toEqual([lineAdded.id]);
expect(rejected.map((r) => r.id)).toEqual([
'8b3e4f5a-6c7d-4e8f-9a0b-1c2d3e4f5a6b',
'9c4f5a6b-7d8e-4f9a-0b1c-2d3e4f5a6b7c',
'not-a-uuid',
null,
]);
expect(rejected[0].reason).toMatch(/quantity/);
});
it('ignores fields it does not know, so newer app versions keep working', () => {
const { accepted } = parseTabEvents([{ ...lineAdded, discountInCents: 50 }]);
expect(accepted).toHaveLength(1);
expect(accepted[0]).not.toHaveProperty('discountInCents');
});
it('rejects line events without a product', () => {
const { productName, ...withoutProduct } = lineAdded;
expect(parseTabEvents([withoutProduct]).rejected).toHaveLength(1);
});
});
describe('tabEventRow', () => {
it('stores removed lines as a negative quantity', () => {
const [event] = parseTabEvents([{ ...lineAdded, type: 'line_removed', quantity: 3 }]).accepted;
expect(tabEventRow(event, 'v1', 'd1', now)).toMatchObject({
venueId: 'v1',
deviceId: 'd1',
type: 'line_removed',
productName: 'Cola',
unitPriceInCents: 250,
quantityDelta: -3,
occurredAt: new Date(lineAdded.occurredAt),
});
});
it('stores a settlement with its amount, payment method and lines', () => {
const [event] = parseTabEvents([settled]).accepted;
const row = tabEventRow(event, 'v1', 'd1', now);
expect(row).toMatchObject({ type: 'settled', amountInCents: 500, paymentMethod: 'payconiq', commandId: null });
expect(row.items).toEqual(settled.items);
expect(row.quantityDelta).toBeUndefined();
});
it('stores JSON null for a settlement without lines', () => {
const { items, ...withoutItems } = settled;
const [event] = parseTabEvents([withoutItems]).accepted;
expect(tabEventRow(event, 'v1', 'd1', now).items).toBe(Prisma.JsonNull);
});
it('uses the receive time when the terminal clock runs ahead', () => {
const [event] = parseTabEvents([{ ...lineAdded, occurredAt: '2026-10-25T03:00:00.000Z' }]).accepted;
expect(tabEventRow(event, 'v1', 'd1', now).occurredAt).toEqual(now);
});
});