feat: add realtime

This commit is contained in:
2026-10-08 04:11:44 +02:00
parent e5580767a5
commit b336580040
31 changed files with 756 additions and 131 deletions
@@ -0,0 +1,103 @@
import { DevicesService } from './devices.service';
type Snapshot = { tabId: string; customerName: string; version: number; totalInCents: number; itemCount: number };
function createService({ previous = [] as Snapshot[], delivered = 0, expired = 0 } = {}) {
const tx = {
tabOwnership: { deleteMany: jest.fn(), upsert: jest.fn() },
openTabSnapshot: { deleteMany: jest.fn(), upsert: jest.fn() },
plannedTab: { updateMany: jest.fn().mockResolvedValue({ count: delivered }) },
device: { update: jest.fn() },
};
const prisma = {
openTabSnapshot: { findMany: jest.fn().mockResolvedValue(previous) },
tabOwnership: { findMany: jest.fn().mockResolvedValue([]) },
$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' }]) },
};
const audit = { log: jest.fn() };
const realtime = { notify: jest.fn() };
const service = new DevicesService(prisma as any, audit as any, realtime as any);
return { service, prisma, tx, realtime };
}
const device = { id: 'd1', venueId: 'v1', revokedAt: null, lastSeenAt: new Date() };
const tab = (id: string, customerName: string) => ({ id, customerName, version: 1, totalInCents: 0, itemCount: 0 });
describe('DevicesService.sync planned customers', () => {
it('marks planned customers delivered once the device reports a tab with their name', async () => {
const { service, tx } = createService();
await service.sync(device, { deviceId: 'd1', openTabs: [tab('t1', 'Jan'), tab('t2', 'Piet')] });
expect(tx.plannedTab.updateMany).toHaveBeenCalledWith({
where: { venueId: 'v1', deliveredAt: null, customerName: { in: ['Jan', 'Piet'] } },
data: { deliveredAt: expect.any(Date) },
});
});
it('only sends customers that have not been delivered yet', async () => {
const { service, prisma } = createService();
const result = await service.sync(device, { deviceId: 'd1', openTabs: [] });
expect(prisma.plannedTab.findMany).toHaveBeenCalledWith(
expect.objectContaining({ where: { venueId: 'v1', deliveredAt: null } }),
);
expect(result.plannedTabNames).toEqual(['Els']);
});
it('does not mark anything delivered when the device reports no tabs', async () => {
const { service, tx } = createService();
await service.sync(device, { deviceId: 'd1', openTabs: [] });
expect(tx.plannedTab.updateMany).not.toHaveBeenCalled();
});
});
describe('DevicesService.sync live update signals', () => {
const jan = { id: 't1', customerName: 'Jan', version: 2, totalInCents: 500, itemCount: 1 };
const janSnapshot = { tabId: 't1', customerName: 'Jan', version: 2, totalInCents: 500, itemCount: 1 };
it('sends nothing when the sync changed nothing', async () => {
const { service, realtime } = createService({ previous: [janSnapshot] });
await service.sync(device, { deviceId: 'd1', openTabs: [jan] });
expect(realtime.notify).not.toHaveBeenCalled();
});
it('signals "tabs" when a tab changed', async () => {
const { service, realtime } = createService({ previous: [janSnapshot] });
await service.sync(device, { deviceId: 'd1', openTabs: [{ ...jan, version: 3, totalInCents: 900, itemCount: 2 }] });
expect(realtime.notify).toHaveBeenCalledWith('v1', 'tabs');
});
it('signals "tabs" when a tab disappeared', async () => {
const { service, realtime } = createService({ previous: [janSnapshot] });
await service.sync(device, { deviceId: 'd1', openTabs: [] });
expect(realtime.notify).toHaveBeenCalledWith('v1', 'tabs');
});
it('signals delivered customers and expired commands', async () => {
const { service, realtime } = createService({ previous: [janSnapshot], delivered: 1, expired: 2 });
await service.sync(device, { deviceId: 'd1', openTabs: [jan] });
expect(realtime.notify).toHaveBeenCalledWith('v1', 'planned-tabs', 'commands');
});
it('signals "device" when a terminal comes back online', async () => {
const { service, realtime } = createService({ previous: [janSnapshot] });
await service.sync({ ...device, lastSeenAt: new Date(Date.now() - 10 * 60 * 1000) }, { deviceId: 'd1', openTabs: [jan] });
expect(realtime.notify).toHaveBeenCalledWith('v1', 'device');
});
});