import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { ProductNotFoundError, InvalidProductStatusError, NoVariantAxesError, AttributeNotFoundError, AttributeValueNotFoundError, ItemCreationFailedError, } from "../lib/errors.generated"; import type { ItemManagementCommands, SkuStrategy } from "../module"; export interface GenerateVariantsInput { productId: string; } /** * Function: generateVariants * * Generates product variants from cartesian product of attribute values. * All attributes are variant axes. */ export async function run( db: Transaction, input: GenerateVariantsInput, ctx: CommandContext, deps: { itemManagement: Pick; skuStrategy: SkuStrategy; }, ) { const { productId } = input; const { itemManagement, skuStrategy } = deps; // Check product exists const product = await db .selectFrom("Product") .selectAll() .where("id", "=", productId) .executeTakeFirst(); if (!product) { return err(new ProductNotFoundError(productId)); } // Must be ACTIVE if (product.status !== "ACTIVE") { return err(new InvalidProductStatusError(productId)); } // Get all attribute assignments for this product const assignments = await db .selectFrom("ProductAttributeAssignment") .selectAll() .where("productId", "=", productId) .execute(); // Group assignments by attribute (all attributes are variant axes) const axisValuesByAttribute = new Map(); for (const assignment of assignments) { const values = axisValuesByAttribute.get(assignment.attributeId) ?? []; values.push(assignment.valueId); axisValuesByAttribute.set(assignment.attributeId, values); } if (axisValuesByAttribute.size === 0) { return err(new NoVariantAxesError(productId)); } // Data integrity check: verify all referenced attributes still exist for (const attributeId of axisValuesByAttribute.keys()) { const attr = await db .selectFrom("ProductAttribute") .select("id") .where("id", "=", attributeId) .executeTakeFirst(); if (!attr) { return err(new AttributeNotFoundError(attributeId)); } } // Data integrity check: verify all referenced values still exist const valueIds = Array.from(new Set(Array.from(axisValuesByAttribute.values()).flat())); const values = await db .selectFrom("ProductAttributeValue") .select(["id", "label"]) .where("id", "in", valueIds) .execute(); const labelsByValueId = new Map(); for (const value of values) { labelsByValueId.set(value.id, value.label); } for (const valueId of valueIds) { if (!labelsByValueId.has(valueId)) { return err(new AttributeValueNotFoundError(valueId)); } } // Compute cartesian product const axisEntries = Array.from(axisValuesByAttribute.values()); const combinations = cartesianProduct(axisEntries); // Get existing variants (with lock for axisValueKey uniqueness) const existingVariants = await db .selectFrom("ProductVariant") .selectAll() .where("productId", "=", productId) .forUpdate() .execute(); const existingKeys = new Set(existingVariants.map((v) => v.axisValueKey)); const newVariants = []; let variantCount = existingVariants.length; for (const combination of combinations) { const sortedValues = [...combination].sort(); const axisValueKey = sortedValues.join("|"); if (existingKeys.has(axisValueKey)) { continue; } // Generate SKU const axisLabels = combination.map((valueId) => labelsByValueId.get(valueId) ?? valueId); const sku = skuStrategy(product.code, combination, variantCount, axisLabels); variantCount++; // Create Item in item-management const itemResult = await itemManagement.createItem( db, { sku, name: `${product.name} - ${sku}`, unitId: product.unitId }, ctx, ); if (!itemResult.ok) { return err(new ItemCreationFailedError(sku)); } const itemId = (itemResult.value as { item: { id: string } }).item.id; // Create ProductVariant record const variant = await db .insertInto("ProductVariant") .values({ productId, axisValueKey, itemId, }) .returningAll() .executeTakeFirstOrThrow(); newVariants.push(variant); } return ok({ variants: newVariants }); } function cartesianProduct(arrays: string[][]): string[][] { if (arrays.length === 0) return [[]]; const [first, ...rest] = arrays; const restCombinations = cartesianProduct(rest); const result: string[][] = []; for (const value of first) { for (const combination of restCombinations) { result.push([value, ...combination]); } } return result; }