fix(intake): normalize OA residential categories
This commit is contained in:
parent
e41966d1fa
commit
e6a4fdf21a
@ -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: "未定义柜体",
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user