Compare commits
1 Commits
main
...
codex/wuyo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
633ed6f598 |
@ -1,69 +1,8 @@
|
||||
-- Structure non-integrated intake SKU rows so SKU master data is not only stored in payload JSON.
|
||||
-- Legacy stock payloads can contain duplicated skuCode values. Preserve those values,
|
||||
-- mark the structured rows as CONFLICT, and let the governance flow clean them later.
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
duplicate_sku_code_groups INTEGER;
|
||||
duplicate_barcode_groups INTEGER;
|
||||
BEGIN
|
||||
WITH payload_sku_items AS (
|
||||
SELECT
|
||||
NULLIF(btrim(item.sku_item ->> 'skuCode'), '') AS sku_code,
|
||||
NULLIF(btrim(item.sku_item ->> 'barcode'), '') AS barcode
|
||||
FROM "IntakeNonIntegratedProduct" p
|
||||
CROSS JOIN LATERAL jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(p."payload"::jsonb -> 'skuItems') = 'array'
|
||||
THEN p."payload"::jsonb -> 'skuItems'
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS item(sku_item)
|
||||
WHERE p."deletedAt" IS NULL
|
||||
),
|
||||
duplicate_sku_codes AS (
|
||||
SELECT sku_code
|
||||
FROM payload_sku_items
|
||||
WHERE sku_code IS NOT NULL
|
||||
GROUP BY sku_code
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
SELECT COUNT(*)::integer
|
||||
INTO duplicate_sku_code_groups
|
||||
FROM duplicate_sku_codes;
|
||||
|
||||
WITH payload_sku_items AS (
|
||||
SELECT
|
||||
NULLIF(btrim(item.sku_item ->> 'skuCode'), '') AS sku_code,
|
||||
NULLIF(btrim(item.sku_item ->> 'barcode'), '') AS barcode
|
||||
FROM "IntakeNonIntegratedProduct" p
|
||||
CROSS JOIN LATERAL jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(p."payload"::jsonb -> 'skuItems') = 'array'
|
||||
THEN p."payload"::jsonb -> 'skuItems'
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS item(sku_item)
|
||||
WHERE p."deletedAt" IS NULL
|
||||
),
|
||||
duplicate_barcodes AS (
|
||||
SELECT barcode
|
||||
FROM payload_sku_items
|
||||
WHERE barcode IS NOT NULL
|
||||
GROUP BY barcode
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
SELECT COUNT(*)::integer
|
||||
INTO duplicate_barcode_groups
|
||||
FROM duplicate_barcodes;
|
||||
|
||||
IF duplicate_sku_code_groups > 0 OR duplicate_barcode_groups > 0 THEN
|
||||
RAISE EXCEPTION
|
||||
'Cannot structure non-integrated SKU items: active payload contains % duplicate skuCode groups and % duplicate barcode groups. Run scripts/sql/preflight-intake-non-integrated-sku-items.sql before this migration.',
|
||||
duplicate_sku_code_groups,
|
||||
duplicate_barcode_groups;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE TYPE "IntakeNonIntegratedSkuDataQualityStatus" AS ENUM ('READY', 'INCOMPLETE');
|
||||
CREATE TYPE "IntakeNonIntegratedSkuDataQualityStatus" AS ENUM ('READY', 'INCOMPLETE', 'CONFLICT');
|
||||
|
||||
CREATE TABLE "IntakeNonIntegratedSkuItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
@ -113,10 +52,12 @@ CREATE UNIQUE INDEX "IntakeNonIntegratedSkuItem_productId_skuItemId_key"
|
||||
ON "IntakeNonIntegratedSkuItem"("productId", "skuItemId");
|
||||
CREATE UNIQUE INDEX "IntakeNonIntegratedSkuItem_skuCode_non_empty_key"
|
||||
ON "IntakeNonIntegratedSkuItem"("skuCode")
|
||||
WHERE NULLIF(btrim("skuCode"), '') IS NOT NULL;
|
||||
WHERE "dataQualityStatus" = 'READY'
|
||||
AND NULLIF(btrim("skuCode"), '') IS NOT NULL;
|
||||
CREATE UNIQUE INDEX "IntakeNonIntegratedSkuItem_barcode_non_empty_key"
|
||||
ON "IntakeNonIntegratedSkuItem"("barcode")
|
||||
WHERE NULLIF(btrim("barcode"), '') IS NOT NULL;
|
||||
WHERE "dataQualityStatus" = 'READY'
|
||||
AND NULLIF(btrim("barcode"), '') IS NOT NULL;
|
||||
CREATE INDEX "IntakeNonIntegratedSkuItem_productId_idx" ON "IntakeNonIntegratedSkuItem"("productId");
|
||||
CREATE INDEX "IntakeNonIntegratedSkuItem_skuCode_idx" ON "IntakeNonIntegratedSkuItem"("skuCode");
|
||||
CREATE INDEX "IntakeNonIntegratedSkuItem_barcode_idx" ON "IntakeNonIntegratedSkuItem"("barcode");
|
||||
@ -175,6 +116,8 @@ normalized_sku_items AS (
|
||||
sku_item,
|
||||
sort_order,
|
||||
sku_item_id,
|
||||
NULLIF(upper(btrim(COALESCE(sku_item ->> 'skuCode', ''))), '') AS normalized_sku_code,
|
||||
NULLIF(upper(btrim(COALESCE(sku_item ->> 'barcode', ''))), '') AS normalized_barcode,
|
||||
array_remove(
|
||||
ARRAY[
|
||||
CASE
|
||||
@ -193,6 +136,17 @@ normalized_sku_items AS (
|
||||
NULL
|
||||
) AS missing_required_fields
|
||||
FROM deduped_sku_items
|
||||
),
|
||||
quality_sku_items AS (
|
||||
SELECT
|
||||
normalized_sku_items.*,
|
||||
COUNT(*) FILTER (WHERE normalized_sku_code IS NOT NULL) OVER (
|
||||
PARTITION BY normalized_sku_code
|
||||
) AS sku_code_occurrences,
|
||||
COUNT(*) FILTER (WHERE normalized_barcode IS NOT NULL) OVER (
|
||||
PARTITION BY normalized_barcode
|
||||
) AS barcode_occurrences
|
||||
FROM normalized_sku_items
|
||||
)
|
||||
INSERT INTO "IntakeNonIntegratedSkuItem" (
|
||||
"id",
|
||||
@ -272,12 +226,14 @@ SELECT
|
||||
COALESCE(sku_item ->> 'sourceSkuId', ''),
|
||||
LOWER(COALESCE(sku_item ->> 'contentLocked', '')) IN ('true', '1', 'yes', 'on'),
|
||||
CASE
|
||||
WHEN sku_code_occurrences > 1 OR barcode_occurrences > 1
|
||||
THEN 'CONFLICT'::"IntakeNonIntegratedSkuDataQualityStatus"
|
||||
WHEN cardinality(missing_required_fields) = 0
|
||||
THEN 'READY'::"IntakeNonIntegratedSkuDataQualityStatus"
|
||||
ELSE 'INCOMPLETE'::"IntakeNonIntegratedSkuDataQualityStatus"
|
||||
END,
|
||||
missing_required_fields
|
||||
FROM normalized_sku_items;
|
||||
FROM quality_sku_items;
|
||||
|
||||
CREATE OR REPLACE VIEW external.intake_sku_items_v1 AS
|
||||
SELECT
|
||||
|
||||
@ -414,12 +414,14 @@ CREATE UNIQUE INDEX IF NOT EXISTS "ProductSkuAssetProfile_barcode_active_non_emp
|
||||
DROP INDEX IF EXISTS "IntakeNonIntegratedSkuItem_skuCode_non_empty_key";
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "IntakeNonIntegratedSkuItem_skuCode_non_empty_key"
|
||||
ON "IntakeNonIntegratedSkuItem"(upper(btrim("skuCode")))
|
||||
WHERE NULLIF(btrim("skuCode"), '') IS NOT NULL;
|
||||
WHERE "dataQualityStatus" = 'READY'
|
||||
AND NULLIF(btrim("skuCode"), '') IS NOT NULL;
|
||||
|
||||
DROP INDEX IF EXISTS "IntakeNonIntegratedSkuItem_barcode_non_empty_key";
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "IntakeNonIntegratedSkuItem_barcode_non_empty_key"
|
||||
ON "IntakeNonIntegratedSkuItem"(upper(btrim("barcode")))
|
||||
WHERE NULLIF(btrim("barcode"), '') IS NOT NULL;
|
||||
WHERE "dataQualityStatus" = 'READY'
|
||||
AND NULLIF(btrim("barcode"), '') IS NOT NULL;
|
||||
|
||||
DROP INDEX IF EXISTS "IntakeReferenceLibraryItem_libraryType_code_non_empty_key";
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "IntakeReferenceLibraryItem_libraryType_code_non_empty_key"
|
||||
|
||||
@ -1433,6 +1433,7 @@ enum ProductSkuAssetSourceStatus {
|
||||
enum IntakeNonIntegratedSkuDataQualityStatus {
|
||||
READY
|
||||
INCOMPLETE
|
||||
CONFLICT
|
||||
}
|
||||
|
||||
enum PackagingSourceType {
|
||||
|
||||
@ -123,6 +123,8 @@ normalized_sku_items AS (
|
||||
sku_item,
|
||||
sort_order,
|
||||
sku_item_id,
|
||||
NULLIF(upper(btrim(COALESCE(sku_item ->> 'skuCode', ''))), '') AS normalized_sku_code,
|
||||
NULLIF(upper(btrim(COALESCE(sku_item ->> 'barcode', ''))), '') AS normalized_barcode,
|
||||
array_remove(
|
||||
ARRAY[
|
||||
CASE
|
||||
@ -141,6 +143,17 @@ normalized_sku_items AS (
|
||||
NULL
|
||||
) AS missing_required_fields
|
||||
FROM deduped_sku_items
|
||||
),
|
||||
quality_sku_items AS (
|
||||
SELECT
|
||||
normalized_sku_items.*,
|
||||
COUNT(*) FILTER (WHERE normalized_sku_code IS NOT NULL) OVER (
|
||||
PARTITION BY normalized_sku_code
|
||||
) AS sku_code_occurrences,
|
||||
COUNT(*) FILTER (WHERE normalized_barcode IS NOT NULL) OVER (
|
||||
PARTITION BY normalized_barcode
|
||||
) AS barcode_occurrences
|
||||
FROM normalized_sku_items
|
||||
)
|
||||
INSERT INTO "IntakeNonIntegratedSkuItem" (
|
||||
"id",
|
||||
@ -222,6 +235,8 @@ SELECT
|
||||
COALESCE(sku_item ->> 'sourceSkuId', ''),
|
||||
LOWER(COALESCE(sku_item ->> 'contentLocked', '')) IN ('true', '1', 'yes', 'on'),
|
||||
CASE
|
||||
WHEN sku_code_occurrences > 1 OR barcode_occurrences > 1
|
||||
THEN 'CONFLICT'::"IntakeNonIntegratedSkuDataQualityStatus"
|
||||
WHEN cardinality(missing_required_fields) = 0
|
||||
THEN 'READY'::"IntakeNonIntegratedSkuDataQualityStatus"
|
||||
ELSE 'INCOMPLETE'::"IntakeNonIntegratedSkuDataQualityStatus"
|
||||
@ -229,7 +244,7 @@ SELECT
|
||||
missing_required_fields,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM normalized_sku_items
|
||||
FROM quality_sku_items
|
||||
ON CONFLICT ("productId", "skuItemId") DO NOTHING;
|
||||
|
||||
CREATE OR REPLACE FUNCTION "cleanup_intake_non_integrated_sku_items_on_soft_delete"()
|
||||
|
||||
@ -37,8 +37,8 @@ WITH required_index_guards(index_name, table_name, predicate_hint, definition_hi
|
||||
('PreSkuBasicInfo_barcode_non_empty_key', 'PreSkuBasicInfo', 'NULLIF(btrim(', 'upper(btrim('),
|
||||
('IntakeNonIntegratedProductProfile_productId_key', 'IntakeNonIntegratedProductProfile', NULL, NULL),
|
||||
('IntakeNonIntegratedSkuItem_productId_skuItemId_key', 'IntakeNonIntegratedSkuItem', NULL, NULL),
|
||||
('IntakeNonIntegratedSkuItem_skuCode_non_empty_key', 'IntakeNonIntegratedSkuItem', 'NULLIF(btrim(', 'upper(btrim('),
|
||||
('IntakeNonIntegratedSkuItem_barcode_non_empty_key', 'IntakeNonIntegratedSkuItem', 'NULLIF(btrim(', 'upper(btrim('),
|
||||
('IntakeNonIntegratedSkuItem_skuCode_non_empty_key', 'IntakeNonIntegratedSkuItem', '"dataQualityStatus" = ''READY''', 'upper(btrim('),
|
||||
('IntakeNonIntegratedSkuItem_barcode_non_empty_key', 'IntakeNonIntegratedSkuItem', '"dataQualityStatus" = ''READY''', 'upper(btrim('),
|
||||
('MasterDataQualityDecision_open_target_key', 'MasterDataQualityDecision', 'status = ''APPROVED_INCOMPLETE''', NULL),
|
||||
('ProductPatentRecord_patentGroupKey_key', 'ProductPatentRecord', NULL, NULL),
|
||||
('IntakeReferenceLibraryItem_libraryType_itemKey_key', 'IntakeReferenceLibraryItem', NULL, NULL),
|
||||
|
||||
@ -514,17 +514,17 @@ const REQUIRED_DATABASE_GUARDS = [
|
||||
type: 'partial-unique-index',
|
||||
name: 'IntakeNonIntegratedSkuItem_skuCode_non_empty_key',
|
||||
relationName: 'IntakeNonIntegratedSkuItem',
|
||||
predicatePattern: 'NULLIF(btrim(',
|
||||
predicatePattern: '"dataQualityStatus" = \'READY\'',
|
||||
definitionPattern: 'upper(btrim(',
|
||||
purpose: '非空 SKU 编码必须全局唯一,空值只允许作为待补齐状态。',
|
||||
purpose: 'READY 状态的非空 SKU 编码必须全局唯一;历史冲突行保留为 CONFLICT 等待治理。',
|
||||
},
|
||||
{
|
||||
type: 'partial-unique-index',
|
||||
name: 'IntakeNonIntegratedSkuItem_barcode_non_empty_key',
|
||||
relationName: 'IntakeNonIntegratedSkuItem',
|
||||
predicatePattern: 'NULLIF(btrim(',
|
||||
predicatePattern: '"dataQualityStatus" = \'READY\'',
|
||||
definitionPattern: 'upper(btrim(',
|
||||
purpose: '非空条码必须全局唯一,空值只允许作为待补齐状态。',
|
||||
purpose: 'READY 状态的非空条码必须全局唯一;历史冲突行保留为 CONFLICT 等待治理。',
|
||||
},
|
||||
{
|
||||
type: 'unique-index',
|
||||
|
||||
@ -370,6 +370,8 @@ export type NonIntegratedSkuItem = {
|
||||
sourceTable?: string;
|
||||
sourceSkuId?: string;
|
||||
contentLocked?: boolean;
|
||||
dataQualityStatus?: "READY" | "INCOMPLETE" | "CONFLICT";
|
||||
missingRequiredFields?: string[];
|
||||
};
|
||||
|
||||
export type NonIntegratedSkuComponentDetail = {
|
||||
|
||||
@ -1070,7 +1070,7 @@ describe('intakeNonIntegratedProductService', () => {
|
||||
const product = createPayload({
|
||||
skuItems: [
|
||||
createSkuItem({ id: 'sku-ready', skuCode: 'SKU-READY', specName: 'Ready', model: 'M-READY' }),
|
||||
createSkuItem({ id: 'sku-incomplete', skuCode: '', specName: '', model: '' }),
|
||||
createSkuItem({ id: 'sku-incomplete', skuCode: '', specName: '', model: '', barcode: '' }),
|
||||
],
|
||||
})
|
||||
|
||||
@ -1089,6 +1089,47 @@ describe('intakeNonIntegratedProductService', () => {
|
||||
).toThrow('产品定厂合同存在未补齐的 SKU 主数据')
|
||||
})
|
||||
|
||||
it('marks duplicated non-integrated SKU identities as conflict', () => {
|
||||
const product = createPayload({
|
||||
skuItems: [
|
||||
createSkuItem({
|
||||
id: 'sku-conflict-a',
|
||||
skuCode: 'dup-sku-001',
|
||||
barcode: 'BAR-CONFLICT-A',
|
||||
specName: 'Conflict A',
|
||||
model: 'MODEL-A',
|
||||
}),
|
||||
createSkuItem({
|
||||
id: 'sku-conflict-b',
|
||||
skuCode: ' DUP-SKU-001 ',
|
||||
barcode: 'BAR-CONFLICT-B',
|
||||
specName: 'Conflict B',
|
||||
model: 'MODEL-B',
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const structuredInputs = buildStructuredSkuItemInputs(product)
|
||||
|
||||
expect(structuredInputs).toEqual([
|
||||
expect.objectContaining({
|
||||
skuCode: 'DUP-SKU-001',
|
||||
dataQualityStatus: 'CONFLICT',
|
||||
missingRequiredFields: [],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
skuCode: 'DUP-SKU-001',
|
||||
dataQualityStatus: 'CONFLICT',
|
||||
missingRequiredFields: [],
|
||||
}),
|
||||
])
|
||||
expect(() =>
|
||||
assertNonIntegratedSkuItemsReadyForFormalUse(product, {
|
||||
usageLabel: '产品定厂合同',
|
||||
}),
|
||||
).toThrow('产品定厂合同存在冲突的 SKU 主数据')
|
||||
})
|
||||
|
||||
it('blocks formal downstream use when non-integrated product profile master data is incomplete', () => {
|
||||
const product = createPayload({
|
||||
productDevelopmentKind: '',
|
||||
|
||||
@ -1937,6 +1937,15 @@ function buildStructuredSkuItemOverlay(
|
||||
sourceTable: readStructuredText(row.sourceTable),
|
||||
sourceSkuId: readStructuredText(row.sourceSkuId),
|
||||
contentLocked: row.contentLocked === true,
|
||||
dataQualityStatus:
|
||||
row.dataQualityStatus === 'CONFLICT' ||
|
||||
row.dataQualityStatus === 'READY' ||
|
||||
row.dataQualityStatus === 'INCOMPLETE'
|
||||
? row.dataQualityStatus
|
||||
: undefined,
|
||||
missingRequiredFields: Array.isArray(row.missingRequiredFields)
|
||||
? row.missingRequiredFields.filter((item): item is string => typeof item === 'string')
|
||||
: undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -3116,13 +3125,35 @@ export function buildStructuredSkuItemInputs(
|
||||
product: NonIntegratedProductRecord & Record<string, unknown>,
|
||||
): StructuredNonIntegratedSkuItemCreateInput[] {
|
||||
const seenSkuItemIds = new Map<string, number>()
|
||||
const skuCodeCounts = new Map<string, number>()
|
||||
const barcodeCounts = new Map<string, number>()
|
||||
|
||||
product.skuItems.forEach((item) => {
|
||||
const skuCode = normalizeSkuIdentityText(item.skuCode)
|
||||
const barcode = normalizeSkuIdentityText(item.barcode)
|
||||
if (skuCode) {
|
||||
skuCodeCounts.set(skuCode, (skuCodeCounts.get(skuCode) ?? 0) + 1)
|
||||
}
|
||||
if (barcode) {
|
||||
barcodeCounts.set(barcode, (barcodeCounts.get(barcode) ?? 0) + 1)
|
||||
}
|
||||
})
|
||||
|
||||
return product.skuItems.map((item, index) => {
|
||||
const rawSkuItemId = trimSkuText(item.id) || `sku-${index + 1}`
|
||||
const seenCount = seenSkuItemIds.get(rawSkuItemId) ?? 0
|
||||
seenSkuItemIds.set(rawSkuItemId, seenCount + 1)
|
||||
const skuItemId = seenCount === 0 ? rawSkuItemId : `${rawSkuItemId}-${seenCount + 1}`
|
||||
const quality = resolveNonIntegratedSkuQuality(item)
|
||||
const normalizedSkuCode = normalizeSkuIdentityText(item.skuCode)
|
||||
const normalizedBarcode = normalizeSkuIdentityText(item.barcode)
|
||||
const quality = resolveNonIntegratedSkuQuality(item, {
|
||||
hasDuplicateSkuCode: normalizedSkuCode
|
||||
? (skuCodeCounts.get(normalizedSkuCode) ?? 0) > 1
|
||||
: false,
|
||||
hasDuplicateBarcode: normalizedBarcode
|
||||
? (barcodeCounts.get(normalizedBarcode) ?? 0) > 1
|
||||
: false,
|
||||
})
|
||||
|
||||
return {
|
||||
id: `${product.id}:${skuItemId}`,
|
||||
|
||||
@ -14,6 +14,10 @@ export const NON_INTEGRATED_PRODUCT_REQUIRED_FIELDS = [
|
||||
] as const
|
||||
|
||||
type NonIntegratedSkuItem = NonIntegratedProductRecord['skuItems'][number]
|
||||
type NonIntegratedSkuQualityOptions = {
|
||||
hasDuplicateSkuCode?: boolean
|
||||
hasDuplicateBarcode?: boolean
|
||||
}
|
||||
|
||||
function trimSkuText(value: unknown) {
|
||||
return typeof value === 'string' ? value.trim() : ''
|
||||
@ -47,15 +51,42 @@ export function resolveNonIntegratedSkuMissingRequiredFields(
|
||||
.map((field) => field.missingField)
|
||||
}
|
||||
|
||||
export function resolveNonIntegratedSkuQuality(item: NonIntegratedSkuItem) {
|
||||
export function resolveNonIntegratedSkuQuality(
|
||||
item: NonIntegratedSkuItem,
|
||||
options: NonIntegratedSkuQualityOptions = {},
|
||||
) {
|
||||
const missingRequiredFields = resolveNonIntegratedSkuMissingRequiredFields(item)
|
||||
const hasIdentityConflict = options.hasDuplicateSkuCode || options.hasDuplicateBarcode
|
||||
|
||||
return {
|
||||
dataQualityStatus: missingRequiredFields.length === 0 ? 'READY' : 'INCOMPLETE',
|
||||
dataQualityStatus: hasIdentityConflict
|
||||
? 'CONFLICT'
|
||||
: missingRequiredFields.length === 0
|
||||
? 'READY'
|
||||
: 'INCOMPLETE',
|
||||
missingRequiredFields,
|
||||
} as const
|
||||
}
|
||||
|
||||
function normalizeSkuIdentity(value: unknown) {
|
||||
return trimSkuText(value).toUpperCase()
|
||||
}
|
||||
|
||||
function countNonEmptyIdentities(
|
||||
items: NonIntegratedSkuItem[],
|
||||
key: 'skuCode' | 'barcode',
|
||||
) {
|
||||
const counts = new Map<string, number>()
|
||||
|
||||
items.forEach((item) => {
|
||||
const identity = normalizeSkuIdentity(item[key])
|
||||
if (!identity) return
|
||||
counts.set(identity, (counts.get(identity) ?? 0) + 1)
|
||||
})
|
||||
|
||||
return counts
|
||||
}
|
||||
|
||||
export function resolveNonIntegratedProductMissingRequiredFields(
|
||||
product: Partial<NonIntegratedProductRecord>,
|
||||
) {
|
||||
@ -109,6 +140,21 @@ export function assertNonIntegratedSkuItemsReadyForFormalUse(
|
||||
const targetSkuItems = product.skuItems.filter((item) =>
|
||||
batch === 'all' ? true : (item.batchNo || 1) === batch,
|
||||
)
|
||||
const skuCodeCounts = countNonEmptyIdentities(targetSkuItems, 'skuCode')
|
||||
const barcodeCounts = countNonEmptyIdentities(targetSkuItems, 'barcode')
|
||||
const conflictItems = targetSkuItems
|
||||
.map((item, index) => ({
|
||||
index,
|
||||
item,
|
||||
hasDuplicateSkuCode: (skuCodeCounts.get(normalizeSkuIdentity(item.skuCode)) ?? 0) > 1,
|
||||
hasDuplicateBarcode: (barcodeCounts.get(normalizeSkuIdentity(item.barcode)) ?? 0) > 1,
|
||||
}))
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.item.dataQualityStatus === 'CONFLICT' ||
|
||||
entry.hasDuplicateSkuCode ||
|
||||
entry.hasDuplicateBarcode,
|
||||
)
|
||||
const incompleteItems = targetSkuItems
|
||||
.map((item, index) => ({
|
||||
index,
|
||||
@ -117,6 +163,32 @@ export function assertNonIntegratedSkuItemsReadyForFormalUse(
|
||||
}))
|
||||
.filter((entry) => entry.missingFields.length > 0)
|
||||
|
||||
if (conflictItems.length > 0) {
|
||||
const sample = conflictItems
|
||||
.slice(0, 3)
|
||||
.map((entry) => {
|
||||
const rowLabel =
|
||||
trimSkuText(entry.item.skuCode) ||
|
||||
trimSkuText(entry.item.barcode) ||
|
||||
trimSkuText(entry.item.model) ||
|
||||
trimSkuText(entry.item.specName) ||
|
||||
`第 ${entry.index + 1} 行`
|
||||
const conflictLabels = [
|
||||
entry.hasDuplicateSkuCode ? 'SKU编码重复' : null,
|
||||
entry.hasDuplicateBarcode ? '条码重复' : null,
|
||||
entry.item.dataQualityStatus === 'CONFLICT' ? '结构化资料冲突' : null,
|
||||
].filter((item): item is string => Boolean(item))
|
||||
|
||||
return `${rowLabel} ${conflictLabels.join('、')}`
|
||||
})
|
||||
.join(';')
|
||||
|
||||
throw new AppError(
|
||||
ErrorCode.VALIDATION_REQUIRED_FIELD,
|
||||
`${options.usageLabel ?? '正式流转'}存在冲突的 SKU 主数据,不能继续:${sample}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (incompleteItems.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user