cangku/backend/test/pickingBinSalesAnalysis.test.js
youruo 817cc74a40 feat: 拣选库位动销分析默认按近 30 天销量划分等级
- 默认统计窗口 15 天 -> 30 天(后端 PICKING_BIN_DEFAULT_DAYS 与前端常量同步)
- 可切换窗口补充近 60 天(7 / 15 / 30 / 60)
- 等级阈值不变:爆款≥20 / 热卖 10-20 / 平销 3-10 / 滞销-1 1-3 / 滞销-2 (0,1) / 销售=0
- 单测断言同步更新;实测 30 天窗口:7091 个拣选库位,日销=销量÷30
2026-10-07 17:41:17 +08:00

122 lines
4.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const {
buildGradeCase,
buildInClause,
fetchPickingBinRows,
} = require('../src/services/pickingBinSalesAnalysis');
const {
PICKING_BIN_GRADE_RULES,
PICKING_BIN_DEFAULT_DAYS,
PICKING_BIN_WINDOW_OPTIONS,
PICKING_BIN_DEFAULT_TYPES,
BIN_TYPES,
} = require('../src/config/pickingBinSalesAnalysis');
test('buildInClause 按位展开参数', () => {
const { clause, params } = buildInClause([101, 102], 'w');
assert.equal(clause, '@w0,@w1');
assert.deepEqual(params, { w0: 101, w1: 102 });
});
test('buildGradeCase 覆盖全部正向等级,并以销售=0 兜底', () => {
const expr = buildGradeCase();
PICKING_BIN_GRADE_RULES.filter((rule) => rule.lower > 0).forEach((rule) => {
assert.ok(expr.includes(`THEN '${rule.key}'`), `${rule.key} 未出现在分档表达式中`);
});
assert.ok(expr.includes("THEN 'slow2'"));
assert.ok(expr.includes("ELSE 'zero'"));
});
test('默认窗口为 30 天,默认只看拣选位', () => {
assert.equal(PICKING_BIN_DEFAULT_DAYS, 30);
assert.ok(PICKING_BIN_WINDOW_OPTIONS.includes(30));
assert.deepEqual(PICKING_BIN_DEFAULT_TYPES, ['拣选']);
assert.deepEqual(BIN_TYPES, ['拣选', '存货']);
});
test('fetchPickingBinRows 首参不是数据库实例时抛错', async () => {
await assert.rejects(
() => fetchPickingBinRows({}, [101], { shipFrom: 'a', shipTo: 'b', days: 15 }),
TypeError
);
});
test('fetchPickingBinRows 无仓库或缺失窗口时直接返回空结果,不打库', async () => {
let called = 0;
const db = { query: async () => { called += 1; return []; } };
assert.deepEqual(await fetchPickingBinRows(db, [], { shipFrom: 'a', shipTo: 'b', days: 15 }), []);
assert.deepEqual(await fetchPickingBinRows(db, [101], { shipFrom: null, shipTo: 'b', days: 15 }), []);
assert.equal(called, 0);
});
test('fetchPickingBinRows 把仓库/类型/窗口参数一并传给业务库,并做分摊与分档', async () => {
let captured = null;
const db = {
query: async (sqlText, params) => {
captured = { sqlText, params };
return [
{ wmsCoId: 101, binName: 'XA01-17-12', binType: '拣选', stockQty: 21, skuCount: 1, salesQty: 9, dailySales: 0.6, gradeKey: 'slow2' },
];
},
};
const rows = await fetchPickingBinRows(db, [101, 102], {
shipFrom: '2026-09-23 01:00:00',
shipTo: '2026-10-08 01:00:00',
days: 15,
binTypes: ['拣选'],
});
assert.equal(rows.length, 1);
assert.deepEqual(rows[0], {
wmsCoId: '101',
binName: 'XA01-17-12',
binType: '拣选',
stockQty: 21,
skuCount: 1,
salesQty: 9,
dailySales: 0.6,
gradeKey: 'slow2',
});
// 参数必须带上两个仓库、发货窗口与天数,以及仓位类型过滤
assert.equal(captured.params.w0, 101);
assert.equal(captured.params.w1, 102);
assert.equal(captured.params.v0, 101);
assert.equal(captured.params.shipFrom, '2026-09-23 01:00:00');
assert.equal(captured.params.shipTo, '2026-10-08 01:00:00');
assert.equal(captured.params.days, 15);
assert.equal(captured.params.t0, '拣选');
// 分摊:SKU 销量除以其占用的同类型仓位数
assert.ok(captured.sqlText.includes('/ NULLIF(sp.binCount, 0)'));
// 仓位类型:bin_group 为空视为拣选位
assert.ok(captured.sqlText.includes("THEN N'拣选'"));
// 只统计有库存的仓位
assert.ok(captured.sqlText.includes('ISNULL(p.qty, 0) <> 0'));
});
test('fetchPickingBinRows 不传类型时不过滤 binType', async () => {
let captured = null;
const db = {
query: async (sqlText, params) => { captured = { sqlText, params }; return []; },
};
await fetchPickingBinRows(db, [101], {
shipFrom: 'a', shipTo: 'b', days: 15, binTypes: [],
});
assert.ok(!captured.sqlText.includes('a.binType IN'));
assert.equal(captured.params.t0, undefined);
});
test('gradeKey 缺失时兜底为 zero', async () => {
const db = { query: async () => [{ wmsCoId: 101, binName: 'X', binType: '拣选', stockQty: 5 }] };
const rows = await fetchPickingBinRows(db, [101], { shipFrom: 'a', shipTo: 'b', days: 15 });
assert.equal(rows[0].gradeKey, 'zero');
assert.equal(rows[0].salesQty, 0);
assert.equal(rows[0].dailySales, 0);
});