import { createContext, DomainError } from "@tailor-platform/erp-kit/app"; import { createResolver, t } from "@tailor-platform/sdk"; import { getDB } from "@/generated/kysely-tailordb"; import { imModulesExport, pmModulesExport } from "@/modules"; export default createResolver({ name: "generateVariants", operation: "mutation", input: { productId: t.string().description("Product ID to generate variants for"), }, body: async (context) => { const ctx = createContext(context); const db = getDB("main-db"); return db .transaction() .execute(async (trx) => { // Generate variants from the cartesian product of assigned attribute values. // erp-kit creates Items (DRAFT) + ProductVariants and gives each Item a // synthetic SKU from the configured skuStrategy (currently `-NNN`). // We rewrite the SKU below to a label-aware form so it joins with // external systems (shipmonk, Trackstar) by SKU equality. const result = await pmModulesExport.commands.generateVariants( trx, { productId: context.input.productId }, ctx, ); if (!result.ok) { throw new Error(`generateVariants failed: ${result.error.code}`); } if (result.value.variants.length === 0) { return { productId: context.input.productId, variantCount: "0", }; } // The Product.code is the SKU prefix. const product = await trx .selectFrom("Product") .select(["code"]) .where("id", "=", context.input.productId) .executeTakeFirstOrThrow(); // axisValueKey is `|`-joined ProductAttributeValue UUIDs that erp-kit // sorts lexicographically before joining. We re-sort by attributeId for // the SKU so the suffix order is stable across variants of the product. const allValueIds = new Set(); for (const v of result.value.variants) { for (const id of v.axisValueKey.split("|")) allValueIds.add(id); } const valueRows = await trx .selectFrom("ProductAttributeValue") .select(["id", "attributeId", "label"]) .where("id", "in", [...allValueIds]) .execute(); const valueById = new Map(valueRows.map((v) => [v.id, v])); // Per-variant: rewrite SKU + barcode, activate. No ItemValuation is // created here: the item's first valuation-affecting movement // auto-assigns the movement company's default valuation policy (or // setItemValuationPolicy assigns one explicitly beforehand). // erp-kit's updateItem command does not expose `sku`, so we use a raw // Kysely UPDATE. The Item.sku unique index is the only enforced // constraint; the type's "immutable" description is prose, not a guard. // Matches canonical denim dimensional labels like "25x30" (digits-x-digits // only). The "x" is stripped from these so they land on the shipmonk / // Trackstar form "2530". Any label that doesn't fit this shape — e.g. // "Luxe", "Oxford", "Max", "20x30x10" — passes through verbatim, so // legitimate "x" characters in non-dimensional labels aren't corrupted. const DIMENSIONAL_LABEL = /^\d+x\d+$/; for (const variant of result.value.variants) { const sortedLabels = variant.axisValueKey .split("|") .map((id) => { const v = valueById.get(id); if (!v) { throw new Error(`ProductAttributeValue not found while resolving SKU: ${id}`); } return { attributeId: v.attributeId, label: v.label, }; }) // Order by attributeId (stable across variants of the same product) // so the SKU suffix order is deterministic — using valueId here would // scramble axis order across variants, since valueId differs per // variant. .sort((a, b) => a.attributeId.localeCompare(b.attributeId)) .map((lv) => DIMENSIONAL_LABEL.test(lv.label) ? lv.label.replaceAll("x", "") : lv.label, ); const sku = product.code + sortedLabels.join(""); await trx .updateTable("Item") .set({ sku, barcode: sku, updatedAt: new Date() }) .where("id", "=", variant.itemId) .execute(); const activateResult = await imModulesExport.commands.activateItem( trx, { id: variant.itemId }, ctx, ); if (!activateResult.ok) { throw new Error( `activateItem failed for item ${variant.itemId}: ${activateResult.error.code}`, ); } } return { productId: context.input.productId, variantCount: String(result.value.variants.length), }; }) .catch((err: unknown) => { if (err instanceof DomainError) throw err; throw new Error("Failed to generate variants", { cause: err }); }); }, output: t .object({ productId: t.string().description("Product ID"), variantCount: t.string().description("Number of variants generated"), }) .description("Generated product variants"), });