import { Prisma } from '../prisma/prisma.service'; /** The fields of a product that the dashboard and the terminals change; stock changes through its own rules. */ export const PRODUCT_FIELDS = [ 'name', 'category', 'priceInCents', 'buyPriceInCents', 'lowStockThreshold', 'desiredStock', 'archived', 'imageId', ] as const; export type ProductField = (typeof PRODUCT_FIELDS)[number]; export type ProductChanges = { name?: string; category?: string | null; priceInCents?: number; buyPriceInCents?: number | null; lowStockThreshold?: number | null; desiredStock?: number | null; archived?: boolean; imageId?: string | null; }; type FieldTimes = Partial>; /** * Merges [changes] made at [at] into a product whose fields were last changed at [times]: per field, the latest * change wins. A price changed on the dashboard and a name changed on an offline terminal both survive; a change * that arrives late (a terminal coming back online) loses to a newer change of the same field. * * Returns the Prisma data to write (empty when nothing wins) and the product's new field times. */ export function mergeProductChanges(times: unknown, changes: ProductChanges, at: Date, current?: { archivedAt: Date | null }) { const previous = (times && typeof times === 'object' ? times : {}) as FieldTimes; const next: FieldTimes = { ...previous }; const data: Prisma.ProductUncheckedUpdateInput = {}; for (const field of PRODUCT_FIELDS) { const value = changes[field]; if (value === undefined) continue; const last = previous[field]; if (last && new Date(last) > at) continue; next[field] = at.toISOString(); if (field === 'archived') { // Archiving again keeps the original archive time data.archivedAt = value ? (current?.archivedAt ?? at) : null; } else { (data as Record)[field] = value; } } return { data, fieldUpdatedAt: next as Prisma.InputJsonObject, changed: Object.keys(data).length > 0 }; } /** Field times for a product created at [at]: every field it was created with. */ export function createdFieldTimes(changes: ProductChanges, at: Date): Prisma.InputJsonObject { return Object.fromEntries(PRODUCT_FIELDS.filter((f) => changes[f] !== undefined).map((f) => [f, at.toISOString()])); } /** * When a change made now on the dashboard happened: now, or just after the latest field time when a terminal whose * clock runs a little ahead stamped one later. A change on the dashboard is made last, so it wins. */ export function dashboardChangeTime(now: Date, times: unknown): Date { const stamps = Object.values((times && typeof times === 'object' ? times : {}) as Record) .map((t) => new Date(t).getTime()) .filter((t) => !Number.isNaN(t)); return new Date(Math.max(now.getTime(), ...stamps)); }