sku builder first test

This commit is contained in:
2026-03-15 22:17:58 -05:00
parent f2b820746a
commit 2718e8b4b1
15 changed files with 1463 additions and 74 deletions

View File

@@ -8,10 +8,16 @@ import { requirePermissions } from "../../lib/rbac.js";
import {
createInventoryItem,
createInventoryReservation,
createInventorySkuFamily,
createInventorySkuNode,
createInventoryTransfer,
createInventoryTransaction,
createWarehouse,
getInventoryItemById,
listInventorySkuCatalog,
listInventorySkuFamilies,
listInventorySkuNodeOptions,
previewInventorySku,
getWarehouseById,
listInventoryItemOptions,
listInventoryItems,
@@ -40,6 +46,12 @@ const operationSchema = z.object({
const inventoryItemSchema = z.object({
sku: z.string().trim().min(1).max(64),
skuBuilder: z
.object({
familyId: z.string().trim().min(1),
nodeId: z.string().trim().min(1).nullable(),
})
.nullable(),
name: z.string().trim().min(1).max(160),
description: z.string(),
type: z.enum(inventoryItemTypes),
@@ -99,6 +111,34 @@ const warehouseSchema = z.object({
locations: z.array(warehouseLocationSchema),
});
const skuFamilySchema = z.object({
code: z.string().trim().min(2).max(12),
sequenceCode: z.string().trim().min(2).max(2),
name: z.string().trim().min(1).max(160),
description: z.string(),
isActive: z.boolean(),
});
const skuNodeSchema = z.object({
familyId: z.string().trim().min(1),
parentNodeId: z.string().trim().min(1).nullable(),
code: z.string().trim().min(1).max(32),
label: z.string().trim().min(1).max(160),
description: z.string(),
sortOrder: z.number().int().nonnegative(),
isActive: z.boolean(),
});
const skuPreviewQuerySchema = z.object({
familyId: z.string().trim().min(1),
nodeId: z.string().trim().min(1).nullable().optional(),
});
const skuNodeOptionsQuerySchema = z.object({
familyId: z.string().trim().min(1),
parentNodeId: z.string().trim().min(1).nullable().optional(),
});
function getRouteParam(value: unknown) {
return typeof value === "string" ? value : null;
}
@@ -125,6 +165,46 @@ inventoryRouter.get("/items/options", requirePermissions([permissions.inventoryR
return ok(response, await listInventoryItemOptions());
});
inventoryRouter.get("/sku/families", requirePermissions([permissions.inventoryRead]), async (_request, response) => {
return ok(response, await listInventorySkuFamilies());
});
inventoryRouter.get("/sku/catalog", requirePermissions([permissions.inventoryRead]), async (_request, response) => {
return ok(response, await listInventorySkuCatalog());
});
inventoryRouter.get("/sku/nodes", requirePermissions([permissions.inventoryRead]), async (request, response) => {
const parsed = skuNodeOptionsQuerySchema.safeParse({
familyId: request.query.familyId,
parentNodeId: request.query.parentNodeId ?? null,
});
if (!parsed.success) {
return fail(response, 400, "INVALID_INPUT", "SKU node filters are invalid.");
}
return ok(response, await listInventorySkuNodeOptions(parsed.data.familyId, parsed.data.parentNodeId ?? null));
});
inventoryRouter.get("/sku/preview", requirePermissions([permissions.inventoryRead]), async (request, response) => {
const parsed = skuPreviewQuerySchema.safeParse({
familyId: request.query.familyId,
nodeId: request.query.nodeId ?? null,
});
if (!parsed.success) {
return fail(response, 400, "INVALID_INPUT", "SKU preview request is invalid.");
}
const preview = await previewInventorySku({
familyId: parsed.data.familyId,
nodeId: parsed.data.nodeId ?? null,
});
if (!preview) {
return fail(response, 400, "INVALID_INPUT", "SKU preview request is invalid.");
}
return ok(response, preview);
});
inventoryRouter.get("/locations/options", requirePermissions([permissions.inventoryRead]), async (_request, response) => {
return ok(response, await listWarehouseLocationOptions());
});
@@ -157,6 +237,34 @@ inventoryRouter.post("/items", requirePermissions([permissions.inventoryWrite]),
return ok(response, item, 201);
});
inventoryRouter.post("/sku/families", requirePermissions([permissions.inventoryWrite]), async (request, response) => {
const parsed = skuFamilySchema.safeParse(request.body);
if (!parsed.success) {
return fail(response, 400, "INVALID_INPUT", "SKU family payload is invalid.");
}
const family = await createInventorySkuFamily(parsed.data);
if (!family) {
return fail(response, 400, "INVALID_INPUT", "SKU family payload is invalid.");
}
return ok(response, family, 201);
});
inventoryRouter.post("/sku/nodes", requirePermissions([permissions.inventoryWrite]), async (request, response) => {
const parsed = skuNodeSchema.safeParse(request.body);
if (!parsed.success) {
return fail(response, 400, "INVALID_INPUT", "SKU branch payload is invalid.");
}
const node = await createInventorySkuNode(parsed.data);
if (!node) {
return fail(response, 400, "INVALID_INPUT", "SKU branch payload is invalid.");
}
return ok(response, node, 201);
});
inventoryRouter.put("/items/:itemId", requirePermissions([permissions.inventoryWrite]), async (request, response) => {
const itemId = getRouteParam(request.params.itemId);
if (!itemId) {

View File

@@ -4,6 +4,14 @@ import type {
InventoryItemDetailDto,
InventoryItemInput,
InventoryItemOperationDto,
InventorySkuBuilderInput,
InventorySkuBuilderPreviewDto,
InventorySkuBuilderSelectionDto,
InventorySkuCatalogTreeDto,
InventorySkuFamilyDto,
InventorySkuFamilyInput,
InventorySkuNodeDto,
InventorySkuNodeInput,
InventoryReservationDto,
InventoryReservationInput,
InventoryReservationStatus,
@@ -58,6 +66,9 @@ type OperationRecord = {
type InventoryDetailRecord = {
id: string;
sku: string;
skuFamilyId: string | null;
skuNodeId: string | null;
skuSequenceNumber: number | null;
name: string;
description: string;
type: string;
@@ -74,6 +85,20 @@ type InventoryDetailRecord = {
notes: string;
createdAt: Date;
updatedAt: Date;
skuFamily: {
id: string;
code: string;
sequenceCode: string;
name: string;
} | null;
skuNode: {
id: string;
code: string;
label: string;
path: string;
level: number;
parentNodeId: string | null;
} | null;
bomLines: BomLineRecord[];
operations: OperationRecord[];
inventoryTransactions: InventoryTransactionRecord[];
@@ -176,6 +201,36 @@ type InventoryTransferRecord = {
} | null;
};
type InventorySkuFamilyRecord = {
id: string;
code: string;
sequenceCode: string;
name: string;
description: string;
nextSequenceNumber: number;
isActive: boolean;
_count: {
nodes: number;
items: number;
};
};
type InventorySkuNodeRecord = {
id: string;
familyId: string;
parentNodeId: string | null;
code: string;
label: string;
description: string;
path: string;
level: number;
sortOrder: number;
isActive: boolean;
_count: {
childNodes: number;
};
};
function mapBomLine(record: BomLineRecord): InventoryBomLineDto {
return {
id: record.id,
@@ -280,6 +335,44 @@ function mapTransfer(record: InventoryTransferRecord): InventoryTransferDto {
};
}
function mapSkuFamily(record: InventorySkuFamilyRecord): InventorySkuFamilyDto {
return {
id: record.id,
code: record.code,
sequenceCode: record.sequenceCode,
name: record.name,
description: record.description,
nextSequenceNumber: record.nextSequenceNumber,
isActive: record.isActive,
childNodeCount: record._count.nodes,
itemCount: record._count.items,
};
}
function mapSkuNode(record: InventorySkuNodeRecord): InventorySkuNodeDto {
return {
id: record.id,
familyId: record.familyId,
parentNodeId: record.parentNodeId,
code: record.code,
label: record.label,
description: record.description,
path: record.path,
level: record.level,
sortOrder: record.sortOrder,
isActive: record.isActive,
childCount: record._count.childNodes,
};
}
function formatSkuSequence(sequenceNumber: number) {
return sequenceNumber.toString().padStart(4, "0");
}
function formatGeneratedSku(sequenceCode: string, segments: string[], sequenceNumber: number) {
return [...segments, `${sequenceCode}${formatSkuSequence(sequenceNumber)}`].join("-");
}
function buildStockBalances(transactions: InventoryTransactionRecord[], reservations: InventoryReservationRecord[]): InventoryStockBalanceDto[] {
const grouped = new Map<string, InventoryStockBalanceDto>();
@@ -390,7 +483,7 @@ function mapSummary(record: {
};
}
function mapDetail(record: InventoryDetailRecord): InventoryItemDetailDto {
function mapDetail(record: InventoryDetailRecord, skuBuilder: InventorySkuBuilderSelectionDto | null): InventoryItemDetailDto {
const recentTransactions = record.inventoryTransactions
.slice()
.sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime())
@@ -429,6 +522,7 @@ function mapDetail(record: InventoryDetailRecord): InventoryItemDetailDto {
defaultPrice: record.defaultPrice,
preferredVendorId: record.preferredVendor?.id ?? null,
preferredVendorName: record.preferredVendor?.name ?? null,
skuBuilder,
notes: record.notes,
createdAt: record.createdAt.toISOString(),
bomLines: record.bomLines.slice().sort((a, b) => a.position - b.position).map(mapBomLine),
@@ -533,6 +627,156 @@ function normalizeWarehouseLocations(locations: WarehouseLocationInput[]) {
.filter((location) => location.code.length > 0 && location.name.length > 0);
}
function normalizeSkuFamilyInput(input: InventorySkuFamilyInput) {
return {
code: input.code.trim().toUpperCase(),
sequenceCode: input.sequenceCode.trim().toUpperCase(),
name: input.name.trim(),
description: input.description.trim(),
isActive: input.isActive,
};
}
function normalizeSkuNodeInput(input: InventorySkuNodeInput) {
return {
familyId: input.familyId,
parentNodeId: input.parentNodeId,
code: input.code.trim(),
label: input.label.trim(),
description: input.description.trim(),
sortOrder: Number(input.sortOrder ?? 0),
isActive: input.isActive,
};
}
async function getSkuNodeLineage(nodeId: string) {
const lineage: Array<{
id: string;
familyId: string;
parentNodeId: string | null;
code: string;
label: string;
path: string;
level: number;
}> = [];
type SkuLineageNode = (typeof lineage)[number];
let currentId: string | null = nodeId;
while (currentId) {
const currentNode: SkuLineageNode | null = await prisma.inventorySkuNode.findUnique({
where: { id: currentId },
select: {
id: true,
familyId: true,
parentNodeId: true,
code: true,
label: true,
path: true,
level: true,
},
});
if (!currentNode) {
return null;
}
lineage.unshift(currentNode);
currentId = currentNode.parentNodeId;
}
return lineage;
}
async function buildSkuBuilderSelection(
family: { id: string; code: string; name: string; sequenceCode: string },
nodeId: string | null,
sequenceNumber: number | null
): Promise<InventorySkuBuilderSelectionDto> {
const lineage = nodeId ? await getSkuNodeLineage(nodeId) : [];
const segments = [family.code, ...(lineage ?? []).map((node) => node.code)];
const effectiveSequenceNumber = sequenceNumber ?? 0;
return {
familyId: family.id,
familyCode: family.code,
familyName: family.name,
sequenceCode: family.sequenceCode,
nodeId,
nodePath: (lineage ?? []).map((node) => ({
id: node.id,
code: node.code,
label: node.label,
level: node.level,
})),
sequenceNumber,
generatedSku: formatGeneratedSku(family.sequenceCode, segments, effectiveSequenceNumber),
segments,
};
}
async function validateSkuBuilder(input: InventorySkuBuilderInput | null) {
if (!input) {
return { ok: true as const, family: null, node: null, segments: [] as string[] };
}
const family = await prisma.inventorySkuFamily.findUnique({
where: { id: input.familyId },
select: {
id: true,
code: true,
name: true,
sequenceCode: true,
nextSequenceNumber: true,
isActive: true,
},
});
if (!family || !family.isActive) {
return { ok: false as const, reason: "Selected SKU family was not found or is inactive." };
}
let node:
| {
id: string;
familyId: string;
code: string;
label: string;
path: string;
level: number;
isActive: boolean;
}
| null = null;
let segments = [family.code];
if (input.nodeId) {
const lineage = await getSkuNodeLineage(input.nodeId);
if (!lineage || lineage.length === 0) {
return { ok: false as const, reason: "Selected SKU branch was not found." };
}
node = await prisma.inventorySkuNode.findUnique({
where: { id: input.nodeId },
select: {
id: true,
familyId: true,
code: true,
label: true,
path: true,
level: true,
isActive: true,
},
});
if (!node || node.familyId !== family.id || !node.isActive || lineage.some((entry) => entry.familyId !== family.id)) {
return { ok: false as const, reason: "Selected SKU branch is invalid for the chosen family." };
}
segments = [family.code, ...lineage.map((entry) => entry.code)];
}
return { ok: true as const, family, node, segments };
}
async function getItemLocationOnHand(itemId: string, warehouseId: string, locationId: string) {
const transactions = await prisma.inventoryTransaction.findMany({
where: {
@@ -672,6 +916,188 @@ async function getActiveReservedQuantity(itemId: string, warehouseId: string, lo
return reservations.reduce((sum, reservation) => sum + reservation.quantity, 0);
}
export async function listInventorySkuFamilies() {
const families = await prisma.inventorySkuFamily.findMany({
include: {
_count: {
select: {
nodes: true,
items: true,
},
},
},
orderBy: [{ code: "asc" }],
});
return families.map(mapSkuFamily);
}
export async function listInventorySkuCatalog(): Promise<InventorySkuCatalogTreeDto> {
const [families, nodes] = await Promise.all([
prisma.inventorySkuFamily.findMany({
include: {
_count: {
select: {
nodes: true,
items: true,
},
},
},
orderBy: [{ code: "asc" }],
}),
prisma.inventorySkuNode.findMany({
include: {
_count: {
select: {
childNodes: true,
},
},
},
orderBy: [{ level: "asc" }, { sortOrder: "asc" }, { code: "asc" }],
}),
]);
return {
families: families.map(mapSkuFamily),
nodes: nodes.map(mapSkuNode),
};
}
export async function listInventorySkuNodeOptions(familyId: string, parentNodeId: string | null = null) {
const nodes = await prisma.inventorySkuNode.findMany({
where: {
familyId,
parentNodeId,
isActive: true,
},
include: {
_count: {
select: {
childNodes: true,
},
},
},
orderBy: [{ sortOrder: "asc" }, { code: "asc" }],
});
return nodes.map(mapSkuNode);
}
export async function previewInventorySku(input: InventorySkuBuilderInput): Promise<InventorySkuBuilderPreviewDto | null> {
const validated = await validateSkuBuilder(input);
if (!validated.ok || !validated.family) {
return null;
}
const childCount = validated.node
? await prisma.inventorySkuNode.count({
where: {
parentNodeId: validated.node.id,
isActive: true,
},
})
: await prisma.inventorySkuNode.count({
where: {
familyId: validated.family.id,
parentNodeId: null,
isActive: true,
},
});
return {
...(await buildSkuBuilderSelection(validated.family, validated.node?.id ?? null, validated.family.nextSequenceNumber)),
nextSequenceNumber: validated.family.nextSequenceNumber,
availableLevels: Math.max(0, 6 - validated.segments.length),
hasChildren: childCount > 0,
};
}
export async function createInventorySkuFamily(input: InventorySkuFamilyInput) {
const payload = normalizeSkuFamilyInput(input);
if (!/^[A-Z0-9]{2,12}$/.test(payload.code) || !/^[A-Z]{2}$/.test(payload.sequenceCode) || payload.name.length === 0) {
return null;
}
const family = await prisma.inventorySkuFamily.create({
data: payload,
include: {
_count: {
select: {
nodes: true,
items: true,
},
},
},
});
return mapSkuFamily(family);
}
export async function createInventorySkuNode(input: InventorySkuNodeInput) {
const payload = normalizeSkuNodeInput(input);
if (!payload.familyId || payload.code.length === 0 || payload.label.length === 0 || payload.code.includes("-")) {
return null;
}
const family = await prisma.inventorySkuFamily.findUnique({
where: { id: payload.familyId },
select: { id: true, code: true, isActive: true },
});
if (!family || !family.isActive) {
return null;
}
let level = 2;
let path = payload.code;
if (payload.parentNodeId) {
const parentNode = await prisma.inventorySkuNode.findUnique({
where: { id: payload.parentNodeId },
select: {
id: true,
familyId: true,
path: true,
level: true,
isActive: true,
},
});
if (!parentNode || parentNode.familyId !== family.id || !parentNode.isActive || parentNode.level >= 6) {
return null;
}
level = parentNode.level + 1;
path = `${parentNode.path}/${payload.code}`;
}
if (level > 6) {
return null;
}
const node = await prisma.inventorySkuNode.create({
data: {
familyId: family.id,
parentNodeId: payload.parentNodeId,
code: payload.code,
label: payload.label,
description: payload.description,
path,
level,
sortOrder: payload.sortOrder,
isActive: payload.isActive,
},
include: {
_count: {
select: {
childNodes: true,
},
},
},
});
return mapSkuNode(node);
}
export async function listInventoryItems(filters: InventoryListFilters = {}) {
const items = await prisma.inventoryItem.findMany({
where: buildWhereClause(filters),
@@ -740,6 +1166,24 @@ export async function getInventoryItemById(itemId: string) {
const item = await prisma.inventoryItem.findUnique({
where: { id: itemId },
include: {
skuFamily: {
select: {
id: true,
code: true,
sequenceCode: true,
name: true,
},
},
skuNode: {
select: {
id: true,
code: true,
label: true,
path: true,
level: true,
parentNodeId: true,
},
},
bomLines: {
include: {
componentItem: {
@@ -862,7 +1306,16 @@ export async function getInventoryItemById(itemId: string) {
},
});
return item ? mapDetail(item) : null;
if (!item) {
return null;
}
const skuBuilder =
item.skuFamily && item.skuSequenceNumber
? await buildSkuBuilderSelection(item.skuFamily, item.skuNode?.id ?? null, item.skuSequenceNumber)
: null;
return mapDetail(item as InventoryDetailRecord, skuBuilder);
}
export async function listWarehouseLocationOptions() {
@@ -1120,35 +1573,89 @@ export async function createInventoryItem(payload: InventoryItemInput, actorId?:
if (!validatedPreferredVendor.ok) {
return null;
}
const validatedSku = await validateSkuBuilder(payload.skuBuilder);
if (!validatedSku.ok) {
return null;
}
const item = await prisma.inventoryItem.create({
data: {
sku: payload.sku,
name: payload.name,
description: payload.description,
type: payload.type,
status: payload.status,
unitOfMeasure: payload.unitOfMeasure,
isSellable: payload.isSellable,
isPurchasable: payload.isPurchasable,
preferredVendorId: payload.preferredVendorId,
defaultCost: payload.defaultCost,
defaultPrice: payload.defaultPrice,
notes: payload.notes,
bomLines: validatedBom.bomLines.length
? {
create: validatedBom.bomLines,
}
: undefined,
operations: validatedOperations.operations.length
? {
create: validatedOperations.operations,
}
: undefined,
},
select: {
id: true,
},
const item = await prisma.$transaction(async (transaction) => {
if (validatedSku.family) {
const sequenceNumber = validatedSku.family.nextSequenceNumber;
await transaction.inventorySkuFamily.update({
where: { id: validatedSku.family.id },
data: {
nextSequenceNumber: {
increment: 1,
},
},
});
return transaction.inventoryItem.create({
data: {
sku: formatGeneratedSku(validatedSku.family.sequenceCode, validatedSku.segments, sequenceNumber),
skuFamilyId: validatedSku.family.id,
skuNodeId: validatedSku.node?.id ?? null,
skuSequenceNumber: sequenceNumber,
name: payload.name,
description: payload.description,
type: payload.type,
status: payload.status,
unitOfMeasure: payload.unitOfMeasure,
isSellable: payload.isSellable,
isPurchasable: payload.isPurchasable,
preferredVendorId: payload.preferredVendorId,
defaultCost: payload.defaultCost,
defaultPrice: payload.defaultPrice,
notes: payload.notes,
bomLines: validatedBom.bomLines.length
? {
create: validatedBom.bomLines,
}
: undefined,
operations: validatedOperations.operations.length
? {
create: validatedOperations.operations,
}
: undefined,
},
select: {
id: true,
},
});
}
return transaction.inventoryItem.create({
data: {
sku: payload.sku,
skuFamilyId: null,
skuNodeId: null,
skuSequenceNumber: null,
name: payload.name,
description: payload.description,
type: payload.type,
status: payload.status,
unitOfMeasure: payload.unitOfMeasure,
isSellable: payload.isSellable,
isPurchasable: payload.isPurchasable,
preferredVendorId: payload.preferredVendorId,
defaultCost: payload.defaultCost,
defaultPrice: payload.defaultPrice,
notes: payload.notes,
bomLines: validatedBom.bomLines.length
? {
create: validatedBom.bomLines,
}
: undefined,
operations: validatedOperations.operations.length
? {
create: validatedOperations.operations,
}
: undefined,
},
select: {
id: true,
},
});
});
await logAuditEvent({
@@ -1156,9 +1663,11 @@ export async function createInventoryItem(payload: InventoryItemInput, actorId?:
entityType: "inventory-item",
entityId: item.id,
action: "created",
summary: `Created inventory item ${payload.sku}.`,
summary: `Created inventory item ${validatedSku.family ? "generated SKU" : payload.sku}.`,
metadata: {
sku: payload.sku,
skuFamilyId: validatedSku.family?.id ?? null,
skuNodeId: validatedSku.node?.id ?? null,
name: payload.name,
type: payload.type,
status: payload.status,
@@ -1189,34 +1698,75 @@ export async function updateInventoryItem(itemId: string, payload: InventoryItem
if (!validatedPreferredVendor.ok) {
return null;
}
const validatedSku = await validateSkuBuilder(payload.skuBuilder);
if (!validatedSku.ok) {
return null;
}
const item = await prisma.inventoryItem.update({
where: { id: itemId },
data: {
sku: payload.sku,
name: payload.name,
description: payload.description,
type: payload.type,
status: payload.status,
unitOfMeasure: payload.unitOfMeasure,
isSellable: payload.isSellable,
isPurchasable: payload.isPurchasable,
preferredVendorId: payload.preferredVendorId,
defaultCost: payload.defaultCost,
defaultPrice: payload.defaultPrice,
notes: payload.notes,
bomLines: {
deleteMany: {},
create: validatedBom.bomLines,
const item = await prisma.$transaction(async (transaction) => {
const shouldKeepExistingGeneratedSku =
validatedSku.family &&
existingItem.skuFamilyId === validatedSku.family.id &&
existingItem.skuNodeId === (validatedSku.node?.id ?? null) &&
existingItem.skuSequenceNumber != null;
let sku = payload.sku;
let skuFamilyId: string | null = null;
let skuNodeId: string | null = null;
let skuSequenceNumber: number | null = null;
if (validatedSku.family) {
skuFamilyId = validatedSku.family.id;
skuNodeId = validatedSku.node?.id ?? null;
if (shouldKeepExistingGeneratedSku) {
sku = existingItem.sku;
skuSequenceNumber = existingItem.skuSequenceNumber;
} else {
skuSequenceNumber = validatedSku.family.nextSequenceNumber;
sku = formatGeneratedSku(validatedSku.family.sequenceCode, validatedSku.segments, skuSequenceNumber);
await transaction.inventorySkuFamily.update({
where: { id: validatedSku.family.id },
data: {
nextSequenceNumber: {
increment: 1,
},
},
});
}
}
return transaction.inventoryItem.update({
where: { id: itemId },
data: {
sku,
skuFamilyId,
skuNodeId,
skuSequenceNumber,
name: payload.name,
description: payload.description,
type: payload.type,
status: payload.status,
unitOfMeasure: payload.unitOfMeasure,
isSellable: payload.isSellable,
isPurchasable: payload.isPurchasable,
preferredVendorId: payload.preferredVendorId,
defaultCost: payload.defaultCost,
defaultPrice: payload.defaultPrice,
notes: payload.notes,
bomLines: {
deleteMany: {},
create: validatedBom.bomLines,
},
operations: {
deleteMany: {},
create: validatedOperations.operations,
},
},
operations: {
deleteMany: {},
create: validatedOperations.operations,
select: {
id: true,
},
},
select: {
id: true,
},
});
});
await logAuditEvent({
@@ -1227,6 +1777,8 @@ export async function updateInventoryItem(itemId: string, payload: InventoryItem
summary: `Updated inventory item ${payload.sku}.`,
metadata: {
sku: payload.sku,
skuFamilyId: validatedSku.family?.id ?? null,
skuNodeId: validatedSku.node?.id ?? null,
name: payload.name,
type: payload.type,
status: payload.status,