import { z } from "zod"; const nullableStr = z.string().nullable().optional(); const nullableNum = z.number().nullable().optional(); const ItemLotTraceOriginSchema = z .object({ stockInLineId: z.number(), type: z.string(), refCode: z.string(), refId: nullableNum, supplierCode: z.string(), supplierName: z.string(), dnNo: z.string(), receiptDate: nullableStr, acceptedQty: z.number(), status: z.string(), }) .passthrough(); const ItemLotTraceQcResultSchema = z .object({ stockInLineId: z.number(), qcPassed: z.boolean(), failQty: z.number(), acceptedQty: z.number(), remarks: z.string(), handledBy: z.string(), created: nullableStr, }) .passthrough(); const ItemLotTracePutawayEventSchema = z .object({ inventoryLotLineId: z.number(), stockInLineId: nullableNum, warehouseCode: z.string(), qty: z.number(), handledBy: z.string(), timestamp: nullableStr, refType: z.string(), refCode: z.string(), refId: nullableNum, status: z.string().optional(), }) .passthrough(); const ItemLotTraceProductionStepSchema = z .object({ processLineId: z.number(), stepName: z.string(), description: z.string(), equipmentCode: z.string(), equipmentName: z.string(), operatorName: z.string(), startTime: nullableStr, endTime: nullableStr, outputQty: z.number(), scrapQty: z.number(), defectQty: z.number(), status: z.string(), seqNo: nullableNum, bomProcessId: nullableNum, }) .passthrough(); const ItemLotTraceMaterialInputSchema: z.ZodType> = z.lazy(() => z .object({ jobOrderCode: z.string(), jobOrderId: nullableNum, materialItemCode: z.string(), materialItemName: z.string(), materialLotNo: z.string(), materialStockInLineId: nullableNum, materialInventoryLotId: nullableNum, materialQty: z.number(), materialUom: z.string().optional(), bomQtyPerUnit: nullableNum, bomProcessId: nullableNum, bomProcessSeqNo: nullableNum, assignedStepName: z.string().optional(), pickOrderCode: z.string(), pickOrderId: nullableNum, consoCode: z.string(), pickedAt: nullableStr, stockInOrigin: ItemLotTraceOriginSchema.nullable().optional(), purchaseEvents: z.array(z.record(z.unknown())).optional(), qcResults: z.array(ItemLotTraceQcResultSchema), putawayEvents: z.array(ItemLotTracePutawayEventSchema).optional(), nestedJoPrelude: ItemLotTraceJoPreludeSchema.nullable().optional(), productionSteps: z.array(ItemLotTraceProductionStepSchema).optional(), }) .passthrough(), ); const ItemLotTraceJoPreludeSchema: z.ZodType> = z.lazy(() => z .object({ jobOrder: z .object({ jobOrderId: z.number(), jobOrderCode: z.string(), planStart: nullableStr, createdAt: nullableStr.optional(), reqQty: z.number(), status: z.string(), }) .passthrough(), pickOrders: z.array(z.record(z.unknown())), materialInputs: z.array(ItemLotTraceMaterialInputSchema), }) .passthrough(), ); const ItemLotTraceGraphEdgeSchema = z .object({ fromKey: z.string(), toKey: z.string(), kind: z.string(), }) .passthrough(); const ItemLotTraceGraphNodeRefSchema = z .object({ key: z.string(), kind: z.string(), scopeKey: nullableStr, refCode: nullableStr, traceLotNo: nullableStr, traceItemCode: nullableStr, warehouseCode: nullableStr, }) .passthrough(); const ItemLotTraceGraphSchema = z .object({ nodes: z.array(ItemLotTraceGraphNodeRefSchema), edges: z.array(ItemLotTraceGraphEdgeSchema), }) .passthrough() .nullable() .optional(); export const ItemLotTraceResponseSchema = z .object({ lot: z .object({ inventoryLotId: z.number(), lotNo: z.string(), itemId: z.number(), itemCode: z.string(), itemName: z.string(), expiryDate: nullableStr, productionDate: nullableStr, stockInDate: nullableStr, uom: z.string(), primaryStockInLineId: nullableNum, }) .passthrough(), warehouseLines: z.array(z.record(z.unknown())), origins: z.array(ItemLotTraceOriginSchema), purchaseEvents: z.array(z.record(z.unknown())).optional(), qcResults: z.array(ItemLotTraceQcResultSchema), putawayEvents: z.array(ItemLotTracePutawayEventSchema), movements: z.array(z.record(z.unknown())), outboundUsage: z.array(z.record(z.unknown())), stockTakeEvents: z.array(z.record(z.unknown())), adjustments: z.array(z.record(z.unknown())), transfers: z.array(z.record(z.unknown())), bomTrace: z.record(z.unknown()), joPrelude: ItemLotTraceJoPreludeSchema.nullable(), productionSteps: z.array(ItemLotTraceProductionStepSchema), byproductLots: z.array(z.record(z.unknown())), openMovements: z.array(z.record(z.unknown())), failEvents: z.array(z.record(z.unknown())), doDeliveries: z.array(z.record(z.unknown())), replenishmentEvents: z.array(z.record(z.unknown())).optional(), returnEvents: z.array(z.record(z.unknown())), lotRelations: z.array(z.record(z.unknown())), alternateLocations: z.array(z.record(z.unknown())), locationBlocks: z.array(z.record(z.unknown())).optional(), traceGraph: ItemLotTraceGraphSchema, issueTrace: z .object({ whereUsed: z.array(z.record(z.unknown())).optional(), downstreamDeliveries: z.array(z.record(z.unknown())).optional(), downstreamReplenishments: z.array(z.record(z.unknown())).optional(), }) .passthrough() .optional(), }) .passthrough(); export type ItemLotTraceResponseParsed = z.infer; export const parseItemLotTraceResponse = (data: unknown): ItemLotTraceResponseParsed => ItemLotTraceResponseSchema.parse(data); /** Collect dot-path keys for contract snapshot tests. */ export const collectObjectPaths = ( obj: unknown, prefix = "", depth = 0, maxDepth = 6, ): string[] => { if (obj == null || depth > maxDepth) return prefix ? [prefix] : []; if (Array.isArray(obj)) { if (obj.length === 0) return prefix ? [prefix] : []; return collectObjectPaths(obj[0], prefix ? `${prefix}[]` : "[]", depth + 1, maxDepth); } if (typeof obj !== "object") return prefix ? [prefix] : []; const paths: string[] = prefix ? [prefix] : []; Object.keys(obj as Record).sort().forEach((key) => { const next = prefix ? `${prefix}.${key}` : key; paths.push(...collectObjectPaths((obj as Record)[key], next, depth + 1, maxDepth)); }); return paths; };