cangku/backend/test/mkhBomService.test.js
2026-09-17 10:26:54 +08:00

172 lines
5.5 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const { _private } = require('../src/services/mkhBomService');
const emptyBom = () => ({ bySku: new Map(), bySpuPrefix: [], entries: [] });
test('official Jushuitan BOM rows use external component codes and consolidate duplicates', () => {
const bom = _private.groupOfficialBomRows([
{
skuId: 'MKHDNZ0409260600022',
componentSkuId: 'DNZ039804N1410V01',
componentName: '桌面',
qty: '1.000000',
},
{
skuId: 'MKHDNZ0409260600022',
componentSkuId: 'DNZ040928N1440V01',
componentName: 'K脚架',
qty: '1.000000',
},
{
skuId: 'MKHDNZ0409260600022',
componentSkuId: 'DNZ040928N1440V01',
componentName: 'K脚架',
qty: '1.000000',
},
{
skuId: 'MKHDNZ0409260600022',
componentSkuId: ' ',
componentName: '无外部编码的内部物料',
qty: 1,
},
]);
const matched = _private.matchComponents(bom, 'MKHDNZ0409260600022');
assert.deepEqual(
matched.components.map((item) => [item.barcode, item.name, item.qty]),
[
['DNZ039804N1410V01', '桌面', 1],
['DNZ040928N1440V01', 'K脚架', 2],
]
);
});
test('business MKH BOM rows are grouped for drill-down and match codes without separators', () => {
const bom = _private.groupBusinessBomRows([
{
comboSkuId: 'MKH-DNY004624N1015V01-1-DNZ038569N1280V01-1',
componentSkuId: 'DNY004624N1015V01',
componentName: '配件 A',
qty: 1,
},
{
comboSkuId: 'MKH-DNY004624N1015V01-1-DNZ038569N1280V01-1',
componentSkuId: 'DNZ038569N1280V01',
componentName: '配件 B',
qty: 2,
},
]);
const matched = _private.matchComponents(
bom,
'MKHDNY004624N1015V011DNZ038569N1280V011'
);
assert.equal(bom.entries.length, 1);
assert.equal(matched.matchType, 'sku');
assert.deepEqual(
matched.components.map((item) => [item.barcode, item.name, item.qty]),
[
['DNY004624N1015V01', '配件 A', 1],
['DNZ038569N1280V01', '配件 B', 2],
]
);
});
test('duplicate business BOM component rows are consolidated without losing quantity', () => {
const bom = _private.groupBusinessBomRows([
{ comboSkuId: 'MKH-A-1', componentSkuId: 'PART-1', qty: 1 },
{ comboSkuId: 'MKH-A-1', componentSkuId: 'PART-1', qty: 2 },
]);
const matched = _private.matchComponents(bom, 'MKHA1');
assert.equal(matched.components.length, 1);
assert.equal(matched.components[0].qty, 3);
});
test('business BOM takes precedence while product database BOM remains a fallback', () => {
const businessBom = _private.groupBusinessBomRows([
{ comboSkuId: 'MKH-A-1', componentSkuId: 'BUSINESS-PART', qty: 1 },
]);
const productBom = {
bySku: new Map([['MKHA1', [{ barcode: 'PRODUCT-PART', qty: 1 }]]]),
bySpuPrefix: [],
entries: [],
};
const matched = _private.matchBomSources(emptyBom(), businessBom, productBom, 'MKHA1');
assert.equal(matched.dataSource, 'business_bom');
assert.equal(matched.components[0].barcode, 'BUSINESS-PART');
});
test('official Jushuitan BOM takes precedence over all fallback sources', () => {
const officialBom = _private.groupOfficialBomRows([
{ skuId: 'MKH-A-1', componentSkuId: 'OFFICIAL-PART', qty: 2 },
]);
const businessBom = _private.groupBusinessBomRows([
{ comboSkuId: 'MKH-A-1', componentSkuId: 'BUSINESS-PART', qty: 1 },
]);
const productBom = {
bySku: new Map([['MKHA1', [{ barcode: 'PRODUCT-PART', qty: 1 }]]]),
bySpuPrefix: [],
entries: [],
};
const matched = _private.matchBomSources(officialBom, businessBom, productBom, 'MKHA1');
assert.equal(matched.dataSource, 'official_bom');
assert.equal(matched.components[0].barcode, 'OFFICIAL-PART');
assert.equal(matched.components[0].qty, 2);
});
test('BOM summary excludes combinations outside the designated warehouse SKU set', () => {
const businessBom = _private.groupBusinessBomRows([
{ comboSkuId: 'MKH-A-1', componentSkuId: 'BUSINESS-PART', qty: 1 },
{ comboSkuId: 'MKH-OUTSIDE-1', componentSkuId: 'OUTSIDE-PART', qty: 1 },
]);
businessBom.existingSkuKeys = new Set(['MKHA1', 'MKHB1']);
const productBom = {
bySku: new Map([['MKHB1', [{ barcode: 'PRODUCT-PART', qty: 1 }]]]),
bySpuPrefix: [],
entries: [],
};
const officialBom = _private.groupOfficialBomRows([
{ skuId: 'MKH-A-1', componentSkuId: 'OFFICIAL-PART', qty: 1 },
]);
assert.deepEqual(_private.summarizeConfiguredBoms(officialBom, businessBom, productBom), {
official: 1,
business: 0,
product: 1,
total: 2,
});
});
test('pack inventory locations match a BOM component and its package suffixes', () => {
assert.equal(_private.matchesPackSku('PART-1', 'PART-1'), true);
assert.equal(_private.matchesPackSku('PART-1', 'PART-1-10'), true);
assert.equal(_private.matchesPackSku('PART-1', 'PART-10'), false);
});
test('pack inventory location prefers bins and falls back to positive-stock box ids', () => {
const locations = _private.buildPackLocationMap([
{
skuId: 'PART-1-1', warehouseId: 1, binGroup: '成品区', bin: 'A-01', packId: 'BOX-1', qty: 2,
},
{
skuId: 'PART-1-2', warehouseId: 1, binGroup: null, bin: '', packId: 'BOX-2', qty: 1,
},
], ['PART-1']);
assert.equal(locations.get('PART-1'), '仓库 1 · 成品区 / A-01、仓库 1 · 箱号 BOX-2');
});
test('components without pack inventory rows do not receive a fallback warehouse location', () => {
const locations = _private.buildPackLocationMap([], ['PART-1']);
assert.equal(locations.has('PART-1'), false);
});