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

This commit is contained in:
2026-10-20 14:45:22 +02:00
parent ee06af8395
commit 1e4438ec5a
24 changed files with 1734 additions and 14 deletions
+91 -1
View File
@@ -3,13 +3,20 @@ import { DevicesService } from './devices.service';
type Snapshot = { tabId: string; customerName: string; version: number; totalInCents: number; itemCount: number; items?: unknown };
function createService({ previous = [] as Snapshot[], delivered = 0, expired = 0 } = {}) {
type StoredProduct = { productId: string; name: string; category: string | null; priceInCents: number; stock: number | null };
function createService({
previous = [] as Snapshot[], delivered = 0, expired = 0, products = [] as StoredProduct[], pendingStock = [] as unknown[],
recordedResults = 1,
} = {}) {
const tx = {
tabOwnership: { deleteMany: jest.fn(), upsert: jest.fn(), updateMany: jest.fn() },
openTabSnapshot: { deleteMany: jest.fn(), upsert: jest.fn(), updateMany: jest.fn() },
plannedTab: { updateMany: jest.fn().mockResolvedValue({ count: delivered }) },
device: { update: jest.fn() },
tabEvent: { createMany: jest.fn() },
product: { deleteMany: jest.fn(), updateMany: jest.fn(), upsert: jest.fn() },
stockAdjustment: { updateMany: jest.fn().mockResolvedValue({ count: recordedResults }) },
};
const prisma = {
openTabSnapshot: { findMany: jest.fn().mockResolvedValue(previous) },
@@ -17,6 +24,8 @@ function createService({ previous = [] as Snapshot[], delivered = 0, expired = 0
$transaction: jest.fn((fn: (t: typeof tx) => unknown) => fn(tx)),
remoteCommand: { updateMany: jest.fn().mockResolvedValue({ count: expired }), findMany: jest.fn().mockResolvedValue([]) },
plannedTab: { findMany: jest.fn().mockResolvedValue([{ customerName: 'Els' }]) },
product: { findMany: jest.fn().mockResolvedValue(products) },
stockAdjustment: { findMany: jest.fn().mockResolvedValue(pendingStock) },
};
const audit = { log: jest.fn() };
const realtime = { notify: jest.fn() };
@@ -201,3 +210,84 @@ describe('DevicesService.sync tab events', () => {
expect(result.acknowledgedEventIds).toEqual([]);
});
});
describe('DevicesService.sync stock', () => {
const cola = { id: 'p1', name: 'Cola', category: 'Frisdrank', priceInCents: 250, stock: 48 };
const storedCola = { productId: 'p1', name: 'Cola', category: 'Frisdrank', priceInCents: 250, stock: 48 };
it('leaves products alone when the terminal does not report them', async () => {
const { service, prisma, tx } = createService({ products: [storedCola] });
await service.sync(device, { deviceId: 'd1', openTabs: [] });
expect(prisma.product.findMany).not.toHaveBeenCalled();
expect(tx.product.deleteMany).not.toHaveBeenCalled();
});
it('only writes changed products, removes missing ones and signals "stock"', async () => {
const storedWater = { productId: 'p2', name: 'Water', category: null, priceInCents: 200, stock: null };
const { service, tx, realtime } = createService({ products: [storedCola, storedWater] });
await service.sync(device, {
deviceId: 'd1',
openTabs: [],
products: [{ ...cola, stock: 46 }, { id: 'p3', name: ' Bier ', priceInCents: 300, stock: null }],
});
expect(tx.product.deleteMany).toHaveBeenCalledWith({ where: { venueId: 'v1', productId: { notIn: ['p1', 'p3'] } } });
expect(tx.product.upsert).toHaveBeenCalledTimes(2);
expect(tx.product.upsert).toHaveBeenCalledWith(expect.objectContaining({
create: expect.objectContaining({ productId: 'p3', name: 'Bier', category: null, stock: null }),
}));
expect(realtime.notify).toHaveBeenCalledWith('v1', 'stock');
});
it('does not signal anything for an unchanged catalog', async () => {
const { service, tx, realtime } = createService({ products: [storedCola], recordedResults: 0 });
await service.sync(device, { deviceId: 'd1', openTabs: [], products: [cola] });
expect(tx.product.upsert).not.toHaveBeenCalled();
expect(realtime.notify).not.toHaveBeenCalled();
});
it('skips an invalid product without failing the sync', async () => {
const { service, tx } = createService();
await service.sync(device, { deviceId: 'd1', openTabs: [], products: [cola, { id: 'p9', name: 'Broken', priceInCents: -1, stock: 3 }] });
expect(tx.product.deleteMany).toHaveBeenCalledWith({ where: { venueId: 'v1', productId: { notIn: ['p1'] } } });
});
it('records stock adjustment results and acknowledges them', async () => {
const { service, tx, realtime } = createService();
const id = '5e0b8b4e-1d2c-4f3a-9b8c-7d6e5f4a3b2c';
const result = await service.sync(device, {
deviceId: 'd1',
openTabs: [],
stockAdjustmentResults: [{ id, status: 'applied', stockAfter: 72 }, { id: 'nope', status: 'applied' }],
});
expect(tx.stockAdjustment.updateMany).toHaveBeenCalledWith({
where: { id, venueId: 'v1', status: { in: ['pending', 'cancelled'] } },
data: { status: 'applied', stockAfter: 72, result: null, processedAt: expect.any(Date) },
});
expect(result.acknowledgedStockAdjustmentResultIds).toEqual([id]);
expect(result.rejectedStockAdjustmentResults.map((r) => r.id)).toEqual(['nope']);
expect(realtime.notify).toHaveBeenCalledWith('v1', 'stock');
});
it('sends the pending stock adjustments of the venue', async () => {
const pending = [{ id: 'a1', productId: 'p1', type: 'add', quantity: 24, note: null, requestedBy: 'Bram', createdAt: new Date() }];
const { service, prisma } = createService({ pendingStock: pending });
const result = await service.sync(device, { deviceId: 'd1', openTabs: [] });
expect(prisma.stockAdjustment.findMany).toHaveBeenCalledWith(expect.objectContaining({
where: { venueId: 'v1', status: 'pending' },
orderBy: { createdAt: 'asc' },
}));
expect(result.stockAdjustments).toEqual(pending);
});
});