This commit is contained in:
@@ -44,6 +44,8 @@ const events = {
|
||||
amountInCents: quantity * 250, paymentMethod: 'payconiq',
|
||||
items: [{ productId: 'cola', productName: 'Cola', quantity, unitPriceInCents: 250 }],
|
||||
}),
|
||||
product: (type: string, productId: string, product: Record<string, unknown> | undefined, minutes = 0) => (
|
||||
{ id: uuidv4(), type, productId, ...(product ? { product } : {}), occurredAt: at(minutes) }),
|
||||
stockSet: (productId: string, quantity: number | null, minutes = 0) => (
|
||||
{ id: uuidv4(), type: 'stock_set', productId, quantity, occurredAt: at(minutes) }),
|
||||
};
|
||||
@@ -531,3 +533,134 @@ describe('DevicesService.sync: website product changes reach the tablets', () =>
|
||||
expect(result.catalog).toEqual([expect.objectContaining({ id: 'cola', name: 'Coca-Cola' })]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DevicesService.sync: products created and changed on a tablet', () => {
|
||||
const admin = { id: 'u1', email: 'admin@example.com', name: 'Admin' };
|
||||
|
||||
function setupWithWebsite() {
|
||||
const venue = setup();
|
||||
const stock = new StockService(venue.prisma as any, { log: jest.fn() } as any, { notify: jest.fn() } as any);
|
||||
return { ...venue, stock };
|
||||
}
|
||||
|
||||
const catalogEntry = (result: SyncResult, id: string) => result.catalog.find((p) => p.id === id);
|
||||
|
||||
it('creates a product made offline, and sells and counts it in the same sync', async () => {
|
||||
const { sync, tabletA, tabletB } = setup();
|
||||
|
||||
await sync(tabletA, {
|
||||
events: [
|
||||
events.product('product_created', 'iced', { name: 'Ice Coffee', category: 'Koffie', priceInCents: 350, lowStockThreshold: 3 }, 1),
|
||||
events.stockSet('iced', 12, 2),
|
||||
events.opened('t1', 'Alice', 3),
|
||||
events.line('line_added', 't1', 2, { minutes: 4, productId: 'iced', productName: 'Ice Coffee', price: 350 }),
|
||||
],
|
||||
});
|
||||
const result = await sync(tabletB);
|
||||
|
||||
expect(catalogEntry(result, 'iced')).toEqual(
|
||||
{ id: 'iced', name: 'Ice Coffee', category: 'Koffie', priceInCents: 350, lowStockThreshold: 3, archived: false, imageId: null });
|
||||
expect(result.stock.find((s) => s.productId === 'iced')?.stock).toBe(10);
|
||||
});
|
||||
|
||||
it('keeps both a website price change and an offline rename of the same product', async () => {
|
||||
const { sync, tabletA, stock, prisma } = setupWithWebsite();
|
||||
await sync(tabletA, { events: [events.product('product_created', 'cola', { name: 'Cola', priceInCents: 250 }, 0)] });
|
||||
|
||||
await stock.updateProduct('v1', 'cola', { priceInCents: 270 }, admin);
|
||||
// Renamed offline before the price change, synced after it
|
||||
const result = await sync(tabletA, { events: [events.product('product_updated', 'cola', { name: 'Coca-Cola' }, 5)] });
|
||||
|
||||
expect(catalogEntry(result, 'cola')).toMatchObject({ name: 'Coca-Cola', priceInCents: 270 });
|
||||
expect(prisma.product.rows[0].fieldUpdatedAt).toMatchObject({ name: at(5) });
|
||||
});
|
||||
|
||||
it('lets the latest change of a field win, also when the older one arrives last', async () => {
|
||||
const { sync, tabletA, tabletB } = setup();
|
||||
await sync(tabletA, { events: [events.product('product_created', 'cola', { name: 'Cola', priceInCents: 250 }, 0)] });
|
||||
|
||||
await sync(tabletB, { events: [events.product('product_updated', 'cola', { priceInCents: 300 }, 20)] });
|
||||
// Tablet A was offline: its older price change arrives later and loses
|
||||
const result = await sync(tabletA, { events: [events.product('product_updated', 'cola', { priceInCents: 280 }, 10)] });
|
||||
|
||||
expect(catalogEntry(result, 'cola')!.priceInCents).toBe(300);
|
||||
});
|
||||
|
||||
it('a website change made after a tablet change wins', async () => {
|
||||
const { sync, tabletA, stock } = setupWithWebsite();
|
||||
await sync(tabletA, { events: [events.product('product_created', 'cola', { name: 'Cola', priceInCents: 250 }, 0)] });
|
||||
await sync(tabletA, { events: [events.product('product_updated', 'cola', { priceInCents: 280 }, 1)] });
|
||||
|
||||
await stock.updateProduct('v1', 'cola', { priceInCents: 260 }, admin);
|
||||
const result = await sync(tabletA);
|
||||
|
||||
expect(catalogEntry(result, 'cola')!.priceInCents).toBe(260);
|
||||
});
|
||||
|
||||
it('archives and restores a product from a tablet', async () => {
|
||||
const { sync, tabletA } = setup();
|
||||
await sync(tabletA, { events: [events.product('product_created', 'cola', { name: 'Cola', priceInCents: 250 }, 0)] });
|
||||
|
||||
const archived = await sync(tabletA, { events: [events.product('product_archived', 'cola', undefined, 1)] });
|
||||
expect(catalogEntry(archived, 'cola')!.archived).toBe(true);
|
||||
|
||||
const restored = await sync(tabletA, { events: [events.product('product_restored', 'cola', undefined, 2)] });
|
||||
expect(catalogEntry(restored, 'cola')!.archived).toBe(false);
|
||||
});
|
||||
|
||||
it('only links a picture that was uploaded', async () => {
|
||||
const { sync, tabletA, prisma } = setup();
|
||||
prisma.productImage.rows.push({ id: 'a'.repeat(64), contentType: 'image/webp', width: 512, height: 512, bytes: Buffer.from('x') });
|
||||
await sync(tabletA, { events: [events.product('product_created', 'cola', { name: 'Cola', priceInCents: 250 }, 0)] });
|
||||
|
||||
const unknown = await sync(tabletA, { events: [events.product('product_updated', 'cola', { imageId: 'b'.repeat(64) }, 1)] });
|
||||
expect(catalogEntry(unknown, 'cola')!.imageId).toBeNull();
|
||||
|
||||
const known = await sync(tabletA, { events: [events.product('product_updated', 'cola', { imageId: 'a'.repeat(64) }, 2)] });
|
||||
expect(catalogEntry(known, 'cola')!.imageId).toBe('a'.repeat(64));
|
||||
|
||||
const removed = await sync(tabletA, { events: [events.product('product_updated', 'cola', { imageId: null }, 3)] });
|
||||
expect(catalogEntry(removed, 'cola')!.imageId).toBeNull();
|
||||
});
|
||||
|
||||
it('accepts two products with the same name from two offline tablets', async () => {
|
||||
const { sync, tabletA, tabletB } = setup();
|
||||
|
||||
await sync(tabletA, { events: [events.product('product_created', 'cola-a', { name: 'Cola', priceInCents: 250 }, 0)] });
|
||||
const result = await sync(tabletB, { events: [events.product('product_created', 'cola-b', { name: 'cola', priceInCents: 250 }, 0)] });
|
||||
|
||||
expect(result.catalog.map((p) => p.id).sort()).toEqual(['cola-a', 'cola-b']);
|
||||
});
|
||||
|
||||
it('acknowledges a change to a product the venue does not have, without creating it', async () => {
|
||||
const { sync, tabletA } = setup();
|
||||
const change = events.product('product_updated', 'gone', { name: 'Gone' }, 0);
|
||||
|
||||
const result = await sync(tabletA, { events: [change] });
|
||||
|
||||
expect(result.acknowledgedEventIds).toEqual([change.id]);
|
||||
expect(result.catalog).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects a new product without a price', async () => {
|
||||
const { sync, tabletA } = setup();
|
||||
const incomplete = events.product('product_created', 'x', { name: 'X' }, 0);
|
||||
|
||||
const result = await sync(tabletA, { events: [incomplete] });
|
||||
|
||||
expect(result.rejectedEvents).toEqual([{ id: incomplete.id, reason: 'a new product needs a name and a price' }]);
|
||||
});
|
||||
|
||||
it('applies a product event once, also when it is sent again', async () => {
|
||||
const { sync, tabletA, tabletB } = setup();
|
||||
const created = events.product('product_created', 'cola', { name: 'Cola', priceInCents: 250 }, 0);
|
||||
const renamed = events.product('product_updated', 'cola', { name: 'Coca-Cola' }, 1);
|
||||
await sync(tabletA, { events: [created, renamed] });
|
||||
await sync(tabletB, { events: [events.product('product_updated', 'cola', { name: 'Cola Classic' }, 2)] });
|
||||
|
||||
// The old rename again, with its id: already applied, so it must not undo the newer name
|
||||
const result = await sync(tabletA, { events: [renamed] });
|
||||
|
||||
expect(catalogEntry(result, 'cola')!.name).toBe('Cola Classic');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user