import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction, Updateable } from "../generated/kysely-tailordb"; import { DuplicateBarcodeError, UnitNotFoundError, ItemNotFoundError, NoFieldsToUpdateError, UomLockedError, } from "../lib/errors.generated"; import { type PrimitivesQueries } from "../module"; export type UpdateItemInput = ( | { id: string; } | { sku: string; } ) & { name?: string; barcode?: string | null; unitId?: string; }; /** * Function: updateItem * * Updates mutable fields of an existing item. * The item can be looked up by id or sku. UoM can only be changed in DRAFT status. */ export async function run>( db: Transaction, input: UpdateItemInput & Omit, "status">, ctx: CommandContext, primitivesQueries: Pick, ) { const { name, barcode, unitId } = input; const ITEM_OWN_KEYS = new Set(["id", "sku", "name", "barcode", "unitId"]); const customFields: Record = {}; for (const [key, value] of Object.entries(input as Record)) { if (!ITEM_OWN_KEYS.has(key)) { customFields[key] = value; } } // 1. Check item exists (lookup by id or sku) const item = "id" in input ? await db .selectFrom("Item") .selectAll() .where("id", "=", (input as { id: string }).id) .forUpdate() .executeTakeFirst() : await db .selectFrom("Item") .selectAll() .where("sku", "=", (input as { sku: string }).sku) .forUpdate() .executeTakeFirst(); if (!item) { const key = "id" in input ? (input as { id: string }).id : (input as { sku: string }).sku; return err(new ItemNotFoundError(key)); } // 2. Check at least one field provided const hasName = name !== undefined; const hasBarcode = barcode !== undefined; const hasUnitId = unitId !== undefined; const hasCustomFields = Object.keys(customFields).length > 0; if (!hasName && !hasBarcode && !hasUnitId && !hasCustomFields) { return err(new NoFieldsToUpdateError(item.id)); } // 3. Check barcode uniqueness when provided (non-null) if (hasBarcode && barcode !== null) { const existingBarcode = await db .selectFrom("Item") .selectAll() .where("barcode", "=", barcode) .where("id", "!=", item.id) .forUpdate() .executeTakeFirst(); if (existingBarcode) { return err(new DuplicateBarcodeError(barcode)); } } // 4. UoM can only be changed in DRAFT status if (hasUnitId) { if (item.status !== "DRAFT") { return err(new UomLockedError(item.id)); } // Validate UoM exists and is active const { unit } = (await primitivesQueries.getUnit(db, { id: unitId }, ctx)).value; if (unit?.status !== "ACTIVE") { return err(new UnitNotFoundError(unitId)); } } // 5. Apply updates const updates: Updateable<"Item"> = { ...(customFields as Updateable<"Item">), }; if (hasName) updates.name = name; if (hasBarcode) updates.barcode = barcode; if (hasUnitId) updates.unitId = unitId; const updatedItem = await db .updateTable("Item") .set(updates) .where("id", "=", item.id) .returningAll() .executeTakeFirstOrThrow(); return ok({ item: updatedItem }); }