import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction, Updateable } from "../generated/kysely-tailordb"; import { ProductNotFoundError, NoFieldsToUpdateError, UomLockedError, InvalidUomError, MissingRequiredFieldError, } from "../lib/errors.generated"; import type { PrimitivesQueries } from "../module"; export type UpdateProductInput = ( | { id: string; } | { code: string; } ) & { name?: string; description?: string | null; unitId?: string; }; /** * Function: updateProduct * * Updates mutable fields of an existing product. * The product can be looked up by id or code. UoM can only be changed in DRAFT status. */ export async function run>( db: Transaction, input: UpdateProductInput & Omit, "status">, ctx: CommandContext, primitivesQueries: Pick, ) { const { name, description, unitId } = input; const KNOWN_KEYS = new Set(["id", "code", "name", "description", "unitId"]); const customFields: Record = {}; for (const [key, value] of Object.entries(input as Record)) { if (!KNOWN_KEYS.has(key)) { customFields[key] = value; } } // 1. Check product exists (lookup by id or code) const product = "id" in input ? await db .selectFrom("Product") .selectAll() .where("id", "=", (input as { id: string }).id) .forUpdate() .executeTakeFirst() : await db .selectFrom("Product") .selectAll() .where("code", "=", (input as { code: string }).code) .forUpdate() .executeTakeFirst(); if (!product) { const key = "id" in input ? (input as { id: string }).id : (input as { code: string }).code; return err(new ProductNotFoundError(key)); } // 2. Check if any fields to update const hasNameUpdate = name !== undefined; const hasDescriptionUpdate = description !== undefined; const hasUnitUpdate = unitId !== undefined; const hasCustomFields = Object.keys(customFields).length > 0; if (!hasNameUpdate && !hasDescriptionUpdate && !hasUnitUpdate && !hasCustomFields) { return err(new NoFieldsToUpdateError(product.id)); } // 3. Validate name if provided if (hasNameUpdate && !name?.trim()) { return err(new MissingRequiredFieldError("name")); } // 4. UoM can only be updated in DRAFT status if (hasUnitUpdate) { if (product.status !== "DRAFT") { return err(new UomLockedError(product.id)); } const unitResult = await primitivesQueries.getUnit(db, { id: unitId }, ctx); if (!unitResult.ok) return err(new InvalidUomError(unitId)); if (unitResult.value.unit?.status !== "ACTIVE") { return err(new InvalidUomError(unitId)); } } // 5. Build update payload const updates: Updateable<"Product"> = { ...(customFields as Updateable<"Product">), }; if (hasNameUpdate) updates.name = name; if (hasDescriptionUpdate) updates.description = description; if (hasUnitUpdate) updates.unitId = unitId; const updated = await db .updateTable("Product") .set(updates) .where("id", "=", product.id) .returningAll() .executeTakeFirstOrThrow(); return ok({ product: updated }); }