fix(intake): normalize OA residential categories

This commit is contained in:
WuYongJie 2026-07-29 11:14:02 +08:00
parent e41966d1fa
commit e6a4fdf21a
3 changed files with 183 additions and 14 deletions

View File

@ -0,0 +1,94 @@
import { buildOaBoardOpeningCodeImportPayload } from "./oaBoardOpeningCodeImport";
import type { OaBoardProjectLookupItem } from "../../core/nonIntegratedApi";
import type { FormValues } from "./productFormValueHelpers";
import type { NonIntegratedOpeningReference } from "../../nonIntegratedStorage";
function createOaItem(overrides: Partial<OaBoardProjectLookupItem> = {}) {
return {
title: "OA 开款",
productName: "",
brand: "",
productCategory: "",
categoryName: "",
openingCode: "XP2026071700000329",
currentStageName: "",
currentApprovalStageName: "",
styleImages: [],
formSummary: {
openingCode: "XP2026071700000329",
demandType: "",
category: "",
categoryName: "",
productPlanner: "",
operationBrandOwner: "",
spuOwner: "",
appearanceDesigner: "",
structureDesigner: "",
purchaseOwner: "",
merchandiserOwner: "",
competitorLink: "",
openingReason: "",
salesPriceBand: "",
platformPriceBand: "",
openingBrand: "",
},
...overrides,
} as OaBoardProjectLookupItem;
}
function createCurrent() {
return {
formValues: {
brand: "",
adaptableBrands: [],
salesCategory: "",
productCategory: "",
} as FormValues,
openingReference: {
adaptableBrands: [],
salesCategory: "",
productCategory: "",
} as NonIntegratedOpeningReference,
};
}
describe("buildOaBoardOpeningCodeImportPayload", () => {
it.each([
["住宅家具 - 鞋柜", "柜类", "鞋柜"],
["住宅家具 - 书架", "架类", "书架"],
])("maps %s to %s / %s", (sourceCategory, salesCategory, productCategory) => {
const item = createOaItem({
formSummary: {
...createOaItem().formSummary,
category: sourceCategory,
categoryName: sourceCategory,
},
});
const result = buildOaBoardOpeningCodeImportPayload(item, createCurrent());
expect(result.formValues).toMatchObject({ salesCategory, productCategory });
expect(result.openingReference).toMatchObject({ salesCategory, productCategory });
});
it("does not retain an unmapped residential path as a product category", () => {
const item = createOaItem({
formSummary: {
...createOaItem().formSummary,
category: "住宅家具 - 未定义柜体",
categoryName: "住宅家具 - 未定义柜体",
},
});
const result = buildOaBoardOpeningCodeImportPayload(item, createCurrent());
expect(result.formValues).toMatchObject({
salesCategory: "",
productCategory: "未定义柜体",
});
expect(result.openingReference).toMatchObject({
salesCategory: "",
productCategory: "未定义柜体",
});
});
});

View File

@ -7,12 +7,22 @@ import {
import type { OaBoardProjectLookupItem } from "../../core/nonIntegratedApi";
import type { FormValues } from "./productFormValueHelpers";
import { BRAND_OPTIONS } from "./basicInfoConfig";
import { getNonIntegratedSecondaryCategoryOptionsForProductCategory } from "@/lib/non-integrated-category-options";
type OaBoardImportPayload = {
formValues: FormValues;
openingReference: NonIntegratedOpeningReference;
};
type ImportedCategoryValues = {
salesCategory: string;
productCategory: string;
hasSalesCategory: boolean;
hasProductCategory: boolean;
};
const RESIDENTIAL_FURNITURE_PREFIX = "住宅家具";
function pickFirstNonEmpty(...values: Array<string | null | undefined>) {
for (const value of values) {
const normalized = String(value ?? "").trim();
@ -44,6 +54,59 @@ function resolveImportedBrand(
return currentBrand || NON_INTEGRATED_DEFAULT_BRAND;
}
function isResidentialFurnitureCategoryPath(value: string) {
return new RegExp(`^${RESIDENTIAL_FURNITURE_PREFIX}\\s*-\\s*`).test(value);
}
function readResidentialFurnitureLeafCategory(value: string) {
return value
.replace(new RegExp(`^${RESIDENTIAL_FURNITURE_PREFIX}\\s*-\\s*`), "")
.trim();
}
function resolveImportedCategories(
item: OaBoardProjectLookupItem,
): ImportedCategoryValues {
const rawSalesCategory = pickFirstNonEmpty(
item.formSummary.category,
item.productCategory,
);
const rawProductCategory = pickFirstNonEmpty(
item.formSummary.categoryName,
item.categoryName,
);
const categoryPath = [rawProductCategory, rawSalesCategory].find(
isResidentialFurnitureCategoryPath,
);
if (!categoryPath) {
return {
salesCategory: rawSalesCategory,
productCategory: rawProductCategory,
hasSalesCategory: Boolean(rawSalesCategory),
hasProductCategory: Boolean(rawProductCategory),
};
}
const leafCategory = readResidentialFurnitureLeafCategory(categoryPath);
const secondaryCategoryOptions =
getNonIntegratedSecondaryCategoryOptionsForProductCategory(leafCategory);
const mappedSecondaryCategory =
secondaryCategoryOptions.length === 1 ? secondaryCategoryOptions[0] : "";
return {
salesCategory: mappedSecondaryCategory ||
(isResidentialFurnitureCategoryPath(rawSalesCategory)
? ""
: rawSalesCategory),
productCategory: isResidentialFurnitureCategoryPath(rawProductCategory)
? leafCategory
: rawProductCategory || leafCategory,
hasSalesCategory: Boolean(rawSalesCategory),
hasProductCategory: Boolean(rawProductCategory) || Boolean(leafCategory),
};
}
export function buildOaBoardOpeningCodeImportPayload(
item: OaBoardProjectLookupItem,
current: OaBoardImportPayload,
@ -59,14 +122,7 @@ export function buildOaBoardOpeningCodeImportPayload(
...current.formValues.adaptableBrands,
...current.openingReference.adaptableBrands,
]).filter((brand) => BRAND_OPTIONS.includes(brand));
const nextSalesCategory = pickFirstNonEmpty(
item.formSummary.category,
item.productCategory,
);
const nextProductCategory = pickFirstNonEmpty(
item.formSummary.categoryName,
item.categoryName,
);
const importedCategories = resolveImportedCategories(item);
const nextProductName = pickFirstNonEmpty(item.productName, item.title);
const nextTargetPriceBand = pickFirstNonEmpty(
item.formSummary.salesPriceBand,
@ -93,9 +149,12 @@ export function buildOaBoardOpeningCodeImportPayload(
productName: nextProductName || current.openingReference.productName,
brand: normalizedBrand,
adaptableBrands: nextAdaptableBrands,
salesCategory: nextSalesCategory || current.openingReference.salesCategory,
productCategory:
nextProductCategory || current.openingReference.productCategory,
salesCategory: importedCategories.hasSalesCategory
? importedCategories.salesCategory
: current.openingReference.salesCategory,
productCategory: importedCategories.hasProductCategory
? importedCategories.productCategory
: current.openingReference.productCategory,
designer:
current.openingReference.designer ||
pickFirstNonEmpty(item.formSummary.appearanceDesigner),
@ -125,9 +184,12 @@ export function buildOaBoardOpeningCodeImportPayload(
pickFirstNonEmpty(styleImage?.alt) || current.formValues.previewImageName,
brand: normalizedBrand,
adaptableBrands: nextAdaptableBrands,
salesCategory: nextSalesCategory || current.formValues.salesCategory,
productCategory:
nextProductCategory || current.formValues.productCategory,
salesCategory: importedCategories.hasSalesCategory
? importedCategories.salesCategory
: current.formValues.salesCategory,
productCategory: importedCategories.hasProductCategory
? importedCategories.productCategory
: current.formValues.productCategory,
designer:
current.formValues.designer ||
pickFirstNonEmpty(item.formSummary.appearanceDesigner),

View File

@ -161,6 +161,19 @@ export function getNonIntegratedProductCategoryOptions(
);
}
export function getNonIntegratedSecondaryCategoryOptionsForProductCategory(
productCategory: string,
) {
const normalizedProductCategory = productCategory.trim();
if (!normalizedProductCategory) return [];
return getUniqueNonEmptyValues(
NON_INTEGRATED_CATEGORY_OPTIONS.filter(
(option) => option.productCategory === normalizedProductCategory,
).map((option) => option.secondaryCategory),
);
}
export function normalizeNonIntegratedProductCategoryForSecondaryCategory(
secondaryCategory: string,
productCategory: string,