@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user