'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { buildGradeCase, buildInClause, fetchGradeRows, fetchZeroSaleSkuRows, } = require('../src/services/stockSalesAnalysis'); const { SALES_GRADE_RULES } = require('../src/config/stockSalesAnalysis'); 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(); SALES_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('fetchGradeRows 首参不是数据库实例时抛错,避免静默返回空数组', async () => { await assert.rejects( () => fetchGradeRows({}, [101], { shipFrom: 'a', shipTo: 'b', days: 30 }), TypeError ); }); test('fetchGradeRows 无仓库或缺失窗口时直接返回空结果', async () => { let called = 0; const db = { query: async () => { called += 1; return []; } }; assert.deepEqual(await fetchGradeRows(db, [], { shipFrom: 'a', shipTo: 'b', days: 30 }), []); assert.deepEqual(await fetchGradeRows(db, [101], { shipFrom: null, shipTo: 'b', days: 30 }), []); assert.equal(called, 0); }); test('fetchGradeRows 把仓库参数与窗口参数一并传给业务库', async () => { let captured = null; const db = { query: async (sqlText, params) => { captured = { sqlText, params }; return [ { wmsCoId: 101, gradeKey: 'hot', skuCount: 2, stockQty: 438, salesQty: 1136, dailySales: 37.87 }, ]; }, }; const rows = await fetchGradeRows(db, [101, 102], { shipFrom: '2026-08-28 01:00:00', shipTo: '2026-09-27 01:00:00', days: 30, }); assert.equal(rows.length, 1); assert.equal(rows[0].wmsCoId, '101'); assert.equal(rows[0].stockQty, 438); assert.equal(captured.params.w0, 101); assert.equal(captured.params.w1, 102); assert.equal(captured.params.v0, 101); assert.equal(captured.params.days, 30); assert.equal(captured.params.shipFrom, '2026-08-28 01:00:00'); assert.equal(captured.params.shipTo, '2026-09-27 01:00:00'); assert.ok(captured.sqlText.includes('@days')); assert.ok(captured.sqlText.includes('rpt.jst_inventoryInfo')); assert.ok(captured.sqlText.includes('rpt.jst_OrderItem')); }); test('fetchZeroSaleSkuRows 首参不是数据库实例时抛错,避免静默返回空数组', async () => { await assert.rejects( () => fetchZeroSaleSkuRows({}, [101], { shipFrom: 'a', shipTo: 'b', days: 30 }), TypeError ); }); test('fetchZeroSaleSkuRows 无仓库或缺失窗口时直接返回空结果', async () => { let called = 0; const db = { query: async () => { called += 1; return []; } }; assert.deepEqual(await fetchZeroSaleSkuRows(db, [], { shipFrom: 'a', shipTo: 'b', days: 30 }), []); assert.deepEqual(await fetchZeroSaleSkuRows(db, [101], { shipFrom: 'a', shipTo: null, days: 30 }), []); assert.equal(called, 0); }); test('fetchZeroSaleSkuRows 复用同一分档口径并关联 SKU 名称', async () => { let captured = null; const db = { query: async (sqlText, params) => { captured = { sqlText, params }; return [ { wmsCoId: 101, skuId: 'SKU-A', skuName: '款式A', binName: 'A01-01-1', stockQty: 7, salesQty: 0 }, { wmsCoId: 101, skuId: 'SKU-A', skuName: '款式A', binName: 'A02-03-1', stockQty: 5, salesQty: 0 }, { wmsCoId: 101, skuId: 'SKU-B', skuName: '款式B', binName: null, stockQty: 3, salesQty: 0 }, ]; }, }; const rows = await fetchZeroSaleSkuRows(db, [101, 102], { shipFrom: '2026-08-28 01:00:00', shipTo: '2026-09-27 01:00:00', days: 30, }); // 每个仓位独立一行;无仓位的 SKU 保留一行(仓位为空、库存取总库存) assert.equal(rows.length, 3); assert.equal(rows[0].wmsCoId, '101'); assert.equal(rows[0].skuName, '款式A'); assert.equal(rows[0].binName, 'A01-01-1'); assert.equal(rows[0].stockQty, 7); assert.equal(rows[1].binName, 'A02-03-1'); assert.equal(rows[1].stockQty, 5); assert.equal(rows[2].binName, ''); assert.equal(rows[2].stockQty, 3); // 口径必须与 fetchGradeRows 一致:同一段 gradeCase 表达式 + 同样的「有库存或有销量」过滤 assert.ok(captured.sqlText.includes("ELSE 'zero'")); assert.ok(captured.sqlText.includes("g.gradeKey = 'zero'")); assert.ok(captured.sqlText.includes('rpt.jst_sku')); assert.ok(captured.sqlText.includes("NOT IN ('Delete', 'Cancelled')")); assert.equal(captured.params.w0, 101); assert.equal(captured.params.days, 30); // 系统仓位:来自 rpt.jst_packInventoryInfo,按「仓库+SKU+仓位」聚合出仓位级库存, // 每仓位一行(COALESCE 兜底无仓位 SKU 取总库存),排除无仓位与零库存记录 assert.ok(captured.sqlText.includes('rpt.jst_packInventoryInfo')); assert.ok(captured.sqlText.includes('COALESCE(bn.binQty, g.stockQty)')); assert.ok(captured.sqlText.includes('binName')); }); test('fetchZeroSaleSkuRows 的仓位必须按同一仓库匹配,避免跨仓错配', async () => { let captured = null; const db = { query: async (sqlText) => { captured = sqlText; return []; }, }; await fetchZeroSaleSkuRows(db, [101], { shipFrom: '2026-08-28 01:00:00', shipTo: '2026-09-27 01:00:00', days: 30, }); assert.ok(captured.includes('bn.wmsCoId = g.wmsCoId AND bn.skuId = g.skuId')); assert.ok(captured.includes("ISNULL(p.qty, 0) <> 0")); });