67 lines
2.8 KiB
TypeScript
67 lines
2.8 KiB
TypeScript
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<Record<ProductField, string>>;
|
|
|
|
/**
|
|
* 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<string, unknown>)[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<string, string>)
|
|
.map((t) => new Date(t).getTime())
|
|
.filter((t) => !Number.isNaN(t));
|
|
return new Date(Math.max(now.getTime(), ...stamps));
|
|
}
|