feat: move to V2
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-10-29 11:21:52 +01:00
parent 6342b8451e
commit 314717ba3a
244 changed files with 11615 additions and 15292 deletions
+66
View File
@@ -0,0 +1,66 @@
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));
}