fix(intake): preserve bom details across barcode updates
This commit is contained in:
parent
368b4427f0
commit
2f02681a69
@ -481,6 +481,107 @@ describe("NonIntegratedDetailClient", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("migrates BOM rows from a temporary barcode to the completed barcode", async () => {
|
||||
saveNonIntegratedProduct(
|
||||
createProductRecord({
|
||||
id: "DNZ0001-bom-barcode-migration",
|
||||
spu: "SJ0384",
|
||||
skuItems: [
|
||||
createSkuItem({
|
||||
id: "sku-sj0384-1",
|
||||
model: "SJ0384K01",
|
||||
skuCode: "SJ038401",
|
||||
barcode: "SJ038401V01",
|
||||
manufacturer: "",
|
||||
manufacturerCode: "",
|
||||
}),
|
||||
],
|
||||
bomDetails: [
|
||||
{
|
||||
styleKey: "K01",
|
||||
skuId: "sku-sj0384-1",
|
||||
barcode: "SJ038401V01",
|
||||
rows: [
|
||||
{
|
||||
id: "bom-temp-barcode-row",
|
||||
category: "閰嶄欢",
|
||||
partCode: "PJ-001",
|
||||
partImage: "",
|
||||
partName: "铰链",
|
||||
structure: "",
|
||||
material: "五金",
|
||||
process: "",
|
||||
remark: "",
|
||||
specSize: "",
|
||||
unit: "个",
|
||||
factor: "",
|
||||
unitPrice: "",
|
||||
unitWeight: "",
|
||||
quantity: "2",
|
||||
},
|
||||
],
|
||||
hiddenRowIds: [],
|
||||
syntheticUnitPriceOverrides: {},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<NonIntegratedDetailClient
|
||||
mode="edit"
|
||||
productId="DNZ0001-bom-barcode-migration"
|
||||
currentUserRole="SUPER_ADMIN"
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const savedProduct = getNonIntegratedProductById(
|
||||
"DNZ0001-bom-barcode-migration",
|
||||
);
|
||||
expect(savedProduct?.bomDetails).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
styleKey: "K01",
|
||||
skuId: "sku-sj0384-1",
|
||||
barcode: "SJ038401V01",
|
||||
rows: expect.arrayContaining([
|
||||
expect.objectContaining({ id: "bom-temp-barcode-row" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
const skuRow = screen
|
||||
.getByDisplayValue("SJ0384K01")
|
||||
.closest("tr");
|
||||
expect(skuRow).not.toBeNull();
|
||||
const supplierInput =
|
||||
within(skuRow!).getByPlaceholderText("搜索供应商名称");
|
||||
const actor = userEvent.setup();
|
||||
await actor.type(supplierInput, "兰考华兰家具");
|
||||
await actor.tab();
|
||||
|
||||
await waitFor(() => {
|
||||
const savedProduct = getNonIntegratedProductById(
|
||||
"DNZ0001-bom-barcode-migration",
|
||||
);
|
||||
expect(savedProduct?.bomDetails).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
styleKey: "K01",
|
||||
skuId: "sku-sj0384-1",
|
||||
barcode: "SJ038401N1280V01",
|
||||
rows: expect.arrayContaining([
|
||||
expect.objectContaining({ id: "bom-temp-barcode-row" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a draft temporary code when saving a new demand without manual temporary code", async () => {
|
||||
const actor = userEvent.setup();
|
||||
const { container } = render(<NonIntegratedDetailClient mode="create" />);
|
||||
|
||||
@ -2926,6 +2926,46 @@ function buildBomDetailStorageKey(styleKey: string, skuId?: string) {
|
||||
return skuId ? `${styleKey}${BOM_DETAIL_SKU_SEPARATOR}${skuId}` : styleKey;
|
||||
}
|
||||
|
||||
function normalizeBomBarcodeKey(value: string | undefined) {
|
||||
return value?.trim().toUpperCase() ?? "";
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function isBomBarcodeForSku(barcode: string, sku: NonIntegratedSkuItem) {
|
||||
const normalizedBarcode = normalizeBomBarcodeKey(barcode);
|
||||
if (!normalizedBarcode) return false;
|
||||
|
||||
if (normalizeBomBarcodeKey(sku.barcode) === normalizedBarcode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return buildAutoBarcodeSkuCandidates(sku.skuCode).some((candidateSkuCode) =>
|
||||
new RegExp(`^${escapeRegExp(candidateSkuCode)}[A-Z0-9]*V01$`).test(
|
||||
normalizedBarcode,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function findSkuForBomBarcode(
|
||||
skuItems: NonIntegratedSkuItem[],
|
||||
barcode: string | undefined,
|
||||
) {
|
||||
const normalizedBarcode = normalizeBomBarcodeKey(barcode);
|
||||
if (!normalizedBarcode) return null;
|
||||
|
||||
return skuItems.find((item) => isBomBarcodeForSku(normalizedBarcode, item)) ?? null;
|
||||
}
|
||||
|
||||
function buildBomDetailBarcodeStorageKey(styleKey: string, barcode?: string) {
|
||||
const normalizedBarcode = normalizeBomBarcodeKey(barcode);
|
||||
return normalizedBarcode
|
||||
? buildBomDetailStorageKey(styleKey, normalizedBarcode)
|
||||
: "";
|
||||
}
|
||||
|
||||
function parseBomDetailStorageKey(
|
||||
detailKey: string,
|
||||
): Pick<NonIntegratedBomDetail, "styleKey" | "skuId"> {
|
||||
@ -2947,6 +2987,103 @@ function hasBomDetailOverride(bomDetails: BomDetailMap, detailKey: string) {
|
||||
return Object.prototype.hasOwnProperty.call(bomDetails, detailKey);
|
||||
}
|
||||
|
||||
function findSkuForBomDetail(
|
||||
skuItems: NonIntegratedSkuItem[],
|
||||
detail: NonIntegratedBomDetail,
|
||||
) {
|
||||
const normalizedBarcode = normalizeBomBarcodeKey(detail.barcode ?? detail.skuId);
|
||||
|
||||
return (
|
||||
findSkuForBomBarcode(skuItems, normalizedBarcode) ??
|
||||
(detail.skuId
|
||||
? skuItems.find((item) => item.id === detail.skuId)
|
||||
: undefined) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function resolveBomDetailStorageKey(
|
||||
skuItems: NonIntegratedSkuItem[],
|
||||
detail: NonIntegratedBomDetail,
|
||||
) {
|
||||
const matchedSku = findSkuForBomDetail(skuItems, detail);
|
||||
const barcodeKey = buildBomDetailBarcodeStorageKey(
|
||||
detail.styleKey,
|
||||
matchedSku?.barcode ?? detail.barcode,
|
||||
);
|
||||
|
||||
if (barcodeKey) return barcodeKey;
|
||||
|
||||
return buildBomDetailStorageKey(detail.styleKey, detail.skuId);
|
||||
}
|
||||
|
||||
function mergeBomDetailState(
|
||||
base: BomDetailState | undefined,
|
||||
incoming: BomDetailState,
|
||||
) {
|
||||
if (!base) {
|
||||
return incoming;
|
||||
}
|
||||
|
||||
const rowIds = new Set(base.rows.map((row) => row.id));
|
||||
return {
|
||||
rows: [
|
||||
...base.rows,
|
||||
...incoming.rows.filter((row) => !rowIds.has(row.id)),
|
||||
],
|
||||
hiddenRowIds: Array.from(
|
||||
new Set([...base.hiddenRowIds, ...incoming.hiddenRowIds]),
|
||||
),
|
||||
syntheticUnitPriceOverrides: {
|
||||
...base.syntheticUnitPriceOverrides,
|
||||
...incoming.syntheticUnitPriceOverrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function migrateBomDetailsToBarcodeKeys(
|
||||
current: BomDetailMap,
|
||||
skuItems: NonIntegratedSkuItem[],
|
||||
) {
|
||||
let changed = false;
|
||||
const nextDetails: BomDetailMap = {};
|
||||
|
||||
for (const [detailKey, detailState] of Object.entries(current)) {
|
||||
const { styleKey, skuId } = parseBomDetailStorageKey(detailKey);
|
||||
if (!styleKey || !skuId) {
|
||||
nextDetails[detailKey] = mergeBomDetailState(
|
||||
nextDetails[detailKey],
|
||||
detailState,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const matchedSku =
|
||||
findSkuForBomBarcode(skuItems, skuId) ??
|
||||
skuItems.find((item) => item.id === skuId);
|
||||
const nextKey = buildBomDetailBarcodeStorageKey(
|
||||
styleKey,
|
||||
matchedSku?.barcode,
|
||||
);
|
||||
|
||||
if (!nextKey || nextKey === detailKey) {
|
||||
nextDetails[detailKey] = mergeBomDetailState(
|
||||
nextDetails[detailKey],
|
||||
detailState,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
changed = true;
|
||||
nextDetails[nextKey] = mergeBomDetailState(
|
||||
nextDetails[nextKey],
|
||||
detailState,
|
||||
);
|
||||
}
|
||||
|
||||
return changed ? nextDetails : current;
|
||||
}
|
||||
|
||||
function mergeBomRows(
|
||||
baseRows: NonIntegratedBomRow[],
|
||||
overrideRows: NonIntegratedBomRow[],
|
||||
@ -3072,14 +3209,18 @@ function createInitialBomDetails(product: NonIntegratedProductRecord | null) {
|
||||
for (const detail of product?.bomDetails ?? []) {
|
||||
const sanitizedRows = sanitizeLegacyInheritedBomRows(detail.rows);
|
||||
hadLegacyCleanup ||= sanitizedRows.hadCleanup;
|
||||
const detailKey = resolveBomDetailStorageKey(
|
||||
product?.skuItems ?? [],
|
||||
detail,
|
||||
);
|
||||
|
||||
nextBomDetails[buildBomDetailStorageKey(detail.styleKey, detail.skuId)] = {
|
||||
nextBomDetails[detailKey] = mergeBomDetailState(nextBomDetails[detailKey], {
|
||||
rows: cloneBomRows(sanitizedRows.rows),
|
||||
hiddenRowIds: Array.from(new Set(detail.hiddenRowIds ?? [])),
|
||||
syntheticUnitPriceOverrides: {
|
||||
...(detail.syntheticUnitPriceOverrides ?? {}),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
for (const item of product?.skuItems ?? []) {
|
||||
@ -5858,6 +5999,15 @@ function buildProductPayload(
|
||||
copiedFromSpu?: string,
|
||||
): NonIntegratedProductRecord {
|
||||
const now = new Date().toISOString();
|
||||
const derivedSkuItemsForPayload = skuItems.map((item, index) => ({
|
||||
...deriveSkuItem(item, index, formValues.spu, formValues),
|
||||
model3dFileName: item.model3dFileName?.trim() ?? "",
|
||||
model3dDataUrl: item.model3dDataUrl?.trim() ?? "",
|
||||
model3dNodeOffsets:
|
||||
item.model3dNodeOffsets ?? EMPTY_PREVIEW_NODE_OFFSET_MAP,
|
||||
model3dNodeRotations:
|
||||
item.model3dNodeRotations ?? EMPTY_PREVIEW_NODE_ROTATION_MAP,
|
||||
}));
|
||||
const normalizedStyleDetails = Object.entries(styleDetails)
|
||||
.filter(([styleKey]) => Boolean(styleKey))
|
||||
.map<NonIntegratedStyleDetail>(([styleKey, detail]) => ({
|
||||
@ -5887,11 +6037,23 @@ function buildProductPayload(
|
||||
packagingModel3dNodeRotations: detail.packagingModel3dNodeRotations,
|
||||
}));
|
||||
const firstStyleDetail = normalizedStyleDetails[0];
|
||||
const normalizedBomDetails = Object.entries(bomDetails)
|
||||
const bomDetailsForPayload = migrateBomDetailsToBarcodeKeys(
|
||||
bomDetails,
|
||||
derivedSkuItemsForPayload,
|
||||
);
|
||||
const normalizedBomDetails = Object.entries(bomDetailsForPayload)
|
||||
.map((entry) => {
|
||||
const [detailKey, detailState] = entry;
|
||||
const { styleKey, skuId } = parseBomDetailStorageKey(detailKey);
|
||||
if (!styleKey) return null;
|
||||
const matchedSku =
|
||||
skuId
|
||||
? findSkuForBomBarcode(derivedSkuItemsForPayload, skuId) ??
|
||||
derivedSkuItemsForPayload.find((item) => item.id === skuId)
|
||||
: null;
|
||||
const normalizedBarcode = normalizeBomBarcodeKey(
|
||||
matchedSku?.barcode ?? skuId,
|
||||
);
|
||||
|
||||
const sanitizedRows = sanitizeLegacyInheritedBomRows(detailState.rows).rows;
|
||||
const normalizedRows = sanitizedRows
|
||||
@ -5990,7 +6152,8 @@ function buildProductPayload(
|
||||
|
||||
return {
|
||||
styleKey,
|
||||
...(skuId ? { skuId } : {}),
|
||||
...(matchedSku?.id ? { skuId: matchedSku.id } : skuId ? { skuId } : {}),
|
||||
...(normalizedBarcode ? { barcode: normalizedBarcode } : {}),
|
||||
rows: normalizedRows,
|
||||
hiddenRowIds: normalizedHiddenRowIds,
|
||||
syntheticUnitPriceOverrides: normalizedSyntheticUnitPriceOverrides,
|
||||
@ -6145,15 +6308,7 @@ function buildProductPayload(
|
||||
return normalizedRow;
|
||||
}),
|
||||
},
|
||||
skuItems: skuItems.map((item, index) => ({
|
||||
...deriveSkuItem(item, index, formValues.spu, formValues),
|
||||
model3dFileName: item.model3dFileName?.trim() ?? "",
|
||||
model3dDataUrl: item.model3dDataUrl?.trim() ?? "",
|
||||
model3dNodeOffsets:
|
||||
item.model3dNodeOffsets ?? EMPTY_PREVIEW_NODE_OFFSET_MAP,
|
||||
model3dNodeRotations:
|
||||
item.model3dNodeRotations ?? EMPTY_PREVIEW_NODE_ROTATION_MAP,
|
||||
})),
|
||||
skuItems: derivedSkuItemsForPayload,
|
||||
factoryRows: normalizeFactoryRowsForPayload(factoryRows),
|
||||
pricingRows: normalizePricingRowsForPayload(pricingRows),
|
||||
sampleMeasurementRows:
|
||||
@ -6824,10 +6979,10 @@ export default function NonIntegratedDetailClient({
|
||||
);
|
||||
const selectedBomStyleKey = selectedStyleKey;
|
||||
const selectedBomSkuKey =
|
||||
selectedBomStyleKey && effectiveSelectedSupplementSkuId
|
||||
? buildBomDetailStorageKey(
|
||||
selectedBomStyleKey && selectedSupplementSku
|
||||
? buildBomDetailBarcodeStorageKey(
|
||||
selectedBomStyleKey,
|
||||
effectiveSelectedSupplementSkuId,
|
||||
selectedSupplementSku.barcode,
|
||||
)
|
||||
: "";
|
||||
const hasSelectedBomOverride =
|
||||
@ -8707,6 +8862,12 @@ export default function NonIntegratedDetailClient({
|
||||
setActiveSharedBomRowIds([]);
|
||||
}, [effectiveSelectedSupplementSkuId, selectedBomStyleKey]);
|
||||
|
||||
useEffect(() => {
|
||||
setBomDetails((current) =>
|
||||
migrateBomDetailsToBarcodeKeys(current, derivedSkuItems),
|
||||
);
|
||||
}, [derivedSkuItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
mode !== "edit" ||
|
||||
@ -9216,21 +9377,37 @@ export default function NonIntegratedDetailClient({
|
||||
setBarcodeScannerSkuId(null);
|
||||
}
|
||||
|
||||
function applySkuSupplierName(
|
||||
item: NonIntegratedSkuItem,
|
||||
index: number,
|
||||
value: string,
|
||||
) {
|
||||
const matchedSupplier = resolveSupplierEntryByName(value);
|
||||
const nextItem = {
|
||||
...item,
|
||||
manufacturer: matchedSupplier ? matchedSupplier.name : value,
|
||||
manufacturerCode: matchedSupplier ? matchedSupplier.code : "",
|
||||
};
|
||||
const nextDerivedItem = deriveSkuItem(
|
||||
nextItem,
|
||||
index,
|
||||
formValues.spu,
|
||||
formValues,
|
||||
);
|
||||
|
||||
return {
|
||||
...nextItem,
|
||||
barcode: nextDerivedItem.barcode,
|
||||
};
|
||||
}
|
||||
|
||||
function updateSkuSupplierName(id: string, value: string) {
|
||||
if (isReadOnly) return;
|
||||
|
||||
const matchedSupplier = resolveSupplierEntryByName(value);
|
||||
|
||||
setSkuItems((current) =>
|
||||
current.map((item) => {
|
||||
if (item.id !== id) return item;
|
||||
|
||||
return {
|
||||
...item,
|
||||
manufacturer: matchedSupplier ? matchedSupplier.name : value,
|
||||
manufacturerCode: matchedSupplier ? matchedSupplier.code : "",
|
||||
};
|
||||
}),
|
||||
current.map((item, index) =>
|
||||
item.id === id ? applySkuSupplierName(item, index, value) : item,
|
||||
),
|
||||
);
|
||||
setSaveMessage("");
|
||||
setSaveMessageType("success");
|
||||
@ -9240,17 +9417,10 @@ export default function NonIntegratedDetailClient({
|
||||
if (isReadOnly) return;
|
||||
|
||||
setSkuItems((current) =>
|
||||
current.map((item) => {
|
||||
current.map((item, index) => {
|
||||
if (item.id !== id) return item;
|
||||
|
||||
const normalizedName = item.manufacturer.trim();
|
||||
const matchedSupplier = resolveSupplierEntryByName(normalizedName);
|
||||
|
||||
return {
|
||||
...item,
|
||||
manufacturer: matchedSupplier ? matchedSupplier.name : normalizedName,
|
||||
manufacturerCode: matchedSupplier ? matchedSupplier.code : "",
|
||||
};
|
||||
return applySkuSupplierName(item, index, item.manufacturer.trim());
|
||||
}),
|
||||
);
|
||||
setSaveMessage("");
|
||||
|
||||
@ -415,6 +415,7 @@ export type NonIntegratedBomRow = {
|
||||
export type NonIntegratedBomDetail = {
|
||||
styleKey: string;
|
||||
skuId?: string;
|
||||
barcode?: string;
|
||||
rows: NonIntegratedBomRow[];
|
||||
hiddenRowIds?: string[];
|
||||
syntheticUnitPriceOverrides?: Partial<
|
||||
@ -1236,6 +1237,7 @@ function normalizeBomDetail(entry: unknown): NonIntegratedBomDetail | null {
|
||||
return {
|
||||
styleKey: item.styleKey,
|
||||
skuId: normalizeString(item.skuId) || undefined,
|
||||
barcode: normalizeString(item.barcode).trim().toUpperCase() || undefined,
|
||||
rows: Array.isArray(item.rows)
|
||||
? item.rows
|
||||
.map((row) => normalizeBomRow(row))
|
||||
|
||||
@ -280,6 +280,103 @@ describe('intakeNonIntegratedProductService', () => {
|
||||
expect(mockedRepository.update).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps existing BOM details when an autosave submits an empty BOM payload', async () => {
|
||||
const existingBomDetails = [
|
||||
{
|
||||
styleKey: 'K01',
|
||||
skuId: 'sku-sj0384-1',
|
||||
barcode: 'SJ038401N1280V01',
|
||||
rows: [
|
||||
{
|
||||
id: 'bom-row-1',
|
||||
category: '配件',
|
||||
partCode: 'PJ-001',
|
||||
partImage: '',
|
||||
partName: '铰链',
|
||||
structure: '',
|
||||
material: '五金',
|
||||
process: '',
|
||||
remark: '',
|
||||
specSize: '',
|
||||
unit: '个',
|
||||
factor: '',
|
||||
unitPrice: '',
|
||||
unitWeight: '',
|
||||
quantity: '2',
|
||||
},
|
||||
],
|
||||
hiddenRowIds: [],
|
||||
syntheticUnitPriceOverrides: {},
|
||||
},
|
||||
]
|
||||
|
||||
const existingRecord = createRecord({
|
||||
payload: createPayload({
|
||||
bomDetails: existingBomDetails,
|
||||
}),
|
||||
})
|
||||
const incomingPayload = createPayload({
|
||||
bomDetails: [],
|
||||
skuItems: [
|
||||
{
|
||||
id: 'sku-sj0384-1',
|
||||
skuSequence: 1,
|
||||
packageGroupId: '',
|
||||
packageSuffix: '',
|
||||
image: '',
|
||||
imageName: '',
|
||||
model: 'SJ0384K01',
|
||||
skuCode: 'SJ038401',
|
||||
setCount: '1',
|
||||
specName: '',
|
||||
barcode: 'SJ038401N1280V01',
|
||||
size: '',
|
||||
color: '',
|
||||
material: '',
|
||||
attributeCode: 'A',
|
||||
drawingId: 'SJ0384K01AV01',
|
||||
installMethod: '',
|
||||
executeStandard: '',
|
||||
manufacturer: '青岛',
|
||||
manufacturerCode: 'N1280',
|
||||
tagMaterial: '',
|
||||
model3dFileName: '',
|
||||
model3dDataUrl: '',
|
||||
model3dNodeOffsets: {},
|
||||
model3dNodeRotations: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
mockedRepository.findBySpu.mockResolvedValue(null as never)
|
||||
mockedRepository.findById.mockResolvedValue(existingRecord as never)
|
||||
mockedRepository.update.mockResolvedValue(existingRecord as never)
|
||||
mockedRepository.findActiveById.mockResolvedValue(existingRecord as never)
|
||||
|
||||
await intakeNonIntegratedProductService.save(incomingPayload, baseUser)
|
||||
|
||||
const updatePayload = mockedRepository.update.mock.calls[0]?.[1]?.payload as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
|
||||
expect(updatePayload?.bomDetails).toEqual([
|
||||
expect.objectContaining({
|
||||
styleKey: 'K01',
|
||||
skuId: 'sku-sj0384-1',
|
||||
barcode: 'SJ038401N1280V01',
|
||||
rows: [
|
||||
expect.objectContaining({
|
||||
id: 'bom-row-1',
|
||||
partCode: 'PJ-001',
|
||||
partName: '铰链',
|
||||
material: '五金',
|
||||
quantity: '2',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('preserves document workspace approval timestamps when saving', async () => {
|
||||
const payload = createPayload({
|
||||
documentWorkspaceConfirmations: {
|
||||
|
||||
@ -333,6 +333,17 @@ function buildHydratedProductRecord(
|
||||
}
|
||||
}
|
||||
|
||||
function hasMeaningfulBomDetails(
|
||||
bomDetails: NonIntegratedProductRecord['bomDetails'] | undefined,
|
||||
) {
|
||||
return (bomDetails ?? []).some(
|
||||
(detail) =>
|
||||
detail.rows.length > 0 ||
|
||||
(detail.hiddenRowIds ?? []).length > 0 ||
|
||||
Object.keys(detail.syntheticUnitPriceOverrides ?? {}).length > 0,
|
||||
)
|
||||
}
|
||||
|
||||
function buildPersistedProductPayload(
|
||||
productInput: unknown,
|
||||
existingPayload?: unknown,
|
||||
@ -348,6 +359,22 @@ function buildPersistedProductPayload(
|
||||
|
||||
const incomingRecord = isRecord(productInput) ? productInput : {}
|
||||
const existingRecord = isRecord(existingPayload) ? existingPayload : {}
|
||||
const normalizedExistingProduct = isRecord(existingPayload)
|
||||
? normalizeNonIntegratedProductRecord(
|
||||
existingPayload as Partial<NonIntegratedProductRecord>,
|
||||
)
|
||||
: null
|
||||
const existingBomDetails = normalizedExistingProduct?.bomDetails ?? []
|
||||
const hasIncomingBomDetails = Array.isArray(incomingRecord.bomDetails)
|
||||
const shouldKeepExistingBomDetails =
|
||||
hasMeaningfulBomDetails(existingBomDetails) &&
|
||||
normalizedProduct.bomDetails.length === 0 &&
|
||||
incomingRecord.clearBomDetails !== true
|
||||
const bomDetails = shouldKeepExistingBomDetails
|
||||
? existingBomDetails
|
||||
: hasIncomingBomDetails || normalizedProduct.bomDetails.length > 0
|
||||
? normalizedProduct.bomDetails
|
||||
: existingBomDetails
|
||||
|
||||
const documentWorkspaceConfirmations = normalizeDocumentWorkspaceConfirmations(
|
||||
'documentWorkspaceConfirmations' in incomingRecord
|
||||
@ -368,9 +395,7 @@ function buildPersistedProductPayload(
|
||||
incomingRecord.uploadFiles,
|
||||
)
|
||||
: mergeUploadFileMetadata(
|
||||
normalizeNonIntegratedProductRecord(
|
||||
existingPayload as Partial<NonIntegratedProductRecord>,
|
||||
)?.uploadFiles ?? [],
|
||||
normalizedExistingProduct?.uploadFiles ?? [],
|
||||
existingRecord.uploadFiles,
|
||||
existingRecord.uploadFiles,
|
||||
)
|
||||
@ -413,6 +438,7 @@ function buildPersistedProductPayload(
|
||||
...existingRecord,
|
||||
...incomingRecord,
|
||||
...normalizedProduct,
|
||||
bomDetails,
|
||||
uploadFiles,
|
||||
documentWorkspaceConfirmations,
|
||||
documentWorkspaceApprovedAt,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user