import assert from 'node:assert/strict'; import test from 'node:test'; import type { AssetAnalysisCell } from '../../shared/assets/analytics'; import { aggregateAnalysisRows, filterAnalysisCells, filterCustomCells, getAnalysisDimensions, getAnalysisSlice, getAvailableDimensions, getCustomAnalysis, getDefaultDimensions, summarizeAnalyticsCells, } from './analytics-model'; function cell(overrides: Partial): AssetAnalysisCell { return { type: '18T', model: '车型甲', region: '华东', city: '嘉兴市', assetStatus: 'inventory', assetDisposition: '在库', operationKind: '可运营', inventoryState: 'available', abnormalType: '不适用', abnormalAge: 'unknown', department: '业务一部', manager: '负责人甲', customer: '客户甲', quantity: 1, ...overrides, }; } const cells = [ cell({ quantity: 5 }), cell({ inventoryState: 'abnormal', abnormalType: '维修', abnormalAge: '30d+', quantity: 3 }), cell({ region: '华南', city: '广州市', inventoryState: 'pending', quantity: 2 }), cell({ assetStatus: 'operating', assetDisposition: '租赁', operationKind: '租赁', inventoryState: 'not-applicable', quantity: 10 }), cell({ type: '49T', region: '华南', city: '深圳市', assetStatus: 'operating', assetDisposition: '自营', operationKind: '自营', inventoryState: 'not-applicable', quantity: 4 }), cell({ assetStatus: 'other', assetDisposition: '其他', operationKind: '未分类', inventoryState: 'not-applicable', quantity: 1 }), ]; test('inventory subset counts reconcile at group and filtered-total levels without adding to inventory', () => { assert.ok(!getAvailableDimensions('inventory').includes('manager')); assert.ok(!getAvailableDimensions('inventory').includes('customer')); assert.ok(getAvailableDimensions('operating').includes('manager')); assert.ok(getAvailableDimensions('operating').includes('customer')); const result = getCustomAnalysis(cells, { view: 'inventory', dimensions: ['region', 'type'] }); assert.equal(result.total, 10); assert.equal(result.summary.pending, 2); assert.equal(result.summary.abnormal, 3); assert.equal(result.rows.reduce((sum, row) => sum + row.totals.pending, 0), 2); assert.equal(result.rows.reduce((sum, row) => sum + row.totals.abnormal, 0), 3); const filtered = getCustomAnalysis(cells, { view: 'inventory', dimensions: ['region', 'type'], filterValues: { region: ['华东'] } }); assert.equal(filtered.total, 8); assert.equal(filtered.summary.pending, 0); assert.equal(filtered.summary.abnormal, 3); assert.equal(filtered.rows[0].totals.pending, 0); assert.equal(filtered.rows[0].totals.abnormal, 3); }); test('management totals reconcile without double-counting inventory subsets', () => { const summary = summarizeAnalyticsCells(cells); assert.equal(summary.assets, 25); assert.equal(summary.operating, 14); assert.equal(summary.inventory, 10); assert.equal(summary.other, 1); assert.equal(summary.abnormal, 3); assert.equal(summary.pending, 2); assert.equal(summary.abnormalRate, 30); assert.equal(summary.pendingRate, 20); assert.ok(Math.abs(summary.operatingRate - 56) < 1e-10); }); test('operating drill keeps region, city, type and operation kind as aggregate levels', () => { let slice = getAnalysisSlice(cells, { view: 'operating' }); assert.equal(slice.dimension, 'region'); assert.equal(slice.total, 14); assert.deepEqual(slice.rows.map(row => row.count), [10, 4]); assert.equal(slice.rows.reduce((sum, row) => sum + row.share, 0), 100); for (const expectedDimension of ['city', 'type', 'operationKind']) { assert.equal(slice.isLeaf, false); slice = getAnalysisSlice(cells, { view: 'operating', path: slice.rows[0].nextPath }); assert.equal(slice.dimension, expectedDimension); assert.equal(slice.total, 10); } slice = getAnalysisSlice(cells, { view: 'operating', path: slice.rows[0].nextPath }); assert.equal(slice.isLeaf, true); assert.equal(slice.dimension, null); assert.deepEqual(slice.rows, []); assert.equal(summarizeAnalyticsCells(filterAnalysisCells(cells, slice.filters, slice.metric)).assets, slice.total); }); test('asset uses mutually exclusive stock, lease and self-operated dispositions', () => { const slice = getAnalysisSlice(cells, { view: 'assets', filters: { type: '18T' } }); assert.equal(slice.dimension, 'assetDisposition'); assert.equal(slice.rows.reduce((sum, row) => sum + row.count, 0), slice.total); assert.deepEqual(slice.rows.map(row => row.value).sort(), ['其他', '在库', '租赁'].sort()); }); test('fixed global dimensions are skipped and conflicting stale paths cannot widen results', () => { const slice = getAnalysisSlice(cells, { view: 'operating', filters: { region: '华南', type: '49T' }, path: [{ dimension: 'region', value: '华南' }, { dimension: 'city', value: '深圳市' }], }); assert.equal(slice.dimension, 'operationKind'); assert.equal(slice.total, 4); assert.deepEqual(slice.path, [{ dimension: 'city', value: '深圳市' }]); const conflict = getAnalysisSlice(cells, { view: 'operating', filters: { region: '华南' }, path: [{ dimension: 'region', value: '华东' }], }); assert.equal(conflict.total, 0); assert.deepEqual(conflict.rows, []); }); test('abnormal pivot retains all three dimensions once and ends at vehicle type', () => { for (const start of ['abnormalType', 'region', 'abnormalAge'] as const) { const dimensions = getAnalysisDimensions('abnormal', start); assert.equal(dimensions[0], start); assert.equal(dimensions.at(-1), 'type'); assert.equal(new Set(dimensions).size, dimensions.length); } const missingTime = cell({ inventoryState: 'abnormal', abnormalType: '维修', abnormalAge: 'unknown', quantity: 2 }); const slice = getAnalysisSlice([...cells, missingTime], { view: 'abnormal', abnormalStart: 'abnormalAge' }); assert.equal(slice.total, 5); assert.equal(slice.rows.find(row => row.value === 'unknown')?.label, '审批时间缺失'); assert.equal(slice.rows.find(row => row.value === 'unknown')?.count, 2); }); test('chronological label sort preserves all duration buckets without rounding totals', () => { const durationCells = ['unknown', '30d+', '0-3d', '15-30d', '7-15d', '3-7d'].map(abnormalAge => cell({ inventoryState: 'abnormal', abnormalAge: abnormalAge as AssetAnalysisCell['abnormalAge'], })); const rows = aggregateAnalysisRows(durationCells, 'abnormalAge', 'abnormal', 'label'); assert.deepEqual(rows.map(row => row.value), ['0-3d', '3-7d', '7-15d', '15-30d', '30d+', 'unknown']); assert.equal(rows.reduce((sum, row) => sum + row.count, 0), 6); assert.equal(summarizeAnalyticsCells([]).operatingRate, 0); }); test('custom analysis combines independent dimensions and conserves the selected metric', () => { const analysis = getCustomAnalysis(cells, { view: 'assets', dimensions: ['region', 'type'] }); assert.deepEqual(analysis.dimensions, ['region', 'type']); assert.equal(analysis.total, 25); assert.equal(analysis.rows.length, 3); assert.equal(analysis.rows.reduce((sum, row) => sum + row.count, 0), analysis.total); assert.equal(analysis.rows.find(row => row.values.region === '华东')?.count, 19); assert.equal(analysis.rows.find(row => row.values.region === '华南' && row.values.type === '18T')?.count, 2); assert.equal(analysis.rows.find(row => row.values.region === '华南' && row.values.type === '49T')?.count, 4); assert.ok(Math.abs(analysis.rows.reduce((sum, row) => sum + row.share, 0) - 100) < 1e-10); }); test('custom composite keys cannot collide when dimension values contain delimiters', () => { const collisionCells = [ cell({ region: 'A · B', city: 'C', quantity: 2 }), cell({ region: 'A', city: 'B · C', quantity: 3 }), cell({ region: '["A","B"]', city: '"C"', quantity: 4 }), ]; const analysis = getCustomAnalysis(collisionCells, { view: 'assets', dimensions: ['region', 'city'] }); assert.equal(analysis.rows.length, 3); assert.equal(new Set(analysis.rows.map(row => row.key)).size, 3); assert.equal(analysis.rows.reduce((sum, row) => sum + row.count, 0), 9); }); test('custom filters OR values within a dimension and AND across dimensions', () => { const filterValues = { region: ['华东', '华南'], type: ['18T'], inventoryState: ['available', 'pending'] }; const filtered = filterCustomCells(cells, filterValues, 'inventory'); assert.equal(summarizeAnalyticsCells(filtered).inventory, 7); const analysis = getCustomAnalysis(cells, { view: 'inventory', dimensions: ['region'], filterValues }); assert.equal(analysis.total, 7); assert.deepEqual(analysis.rows.map(row => row.count), [5, 2]); assert.equal(analysis.rows[0].share, 5 / 7 * 100); assert.equal(getCustomAnalysis(cells, { view: 'inventory', dimensions: ['region'], filterValues: { region: ['不存在区域'] } }).total, 0); }); test('empty filter selections impose no restriction and filter-only analysis gives one total', () => { const analysis = getCustomAnalysis(cells, { view: 'operating', dimensions: [], filterValues: { region: [] } }); assert.equal(analysis.total, 14); assert.equal(analysis.rows.length, 1); assert.equal(analysis.rows[0].label, '合计'); assert.equal(analysis.rows[0].count, 14); assert.equal(analysis.rows[0].share, 100); assert.deepEqual(analysis.rows[0].values, {}); assert.equal(analysis.summary.assets, 25); assert.equal(getCustomAnalysis([], { view: 'operating', dimensions: [] }).rows.length, 0); assert.equal(getCustomAnalysis([cell({})], { view: 'operating', dimensions: [] }).rows.length, 0); }); test('custom shares use the selected metric rather than all assets in the scope', () => { const analysis = getCustomAnalysis(cells, { view: 'operating', dimensions: ['region'] }); assert.equal(analysis.total, 14); assert.equal(analysis.summary.assets, 25); assert.equal(analysis.rows.find(row => row.values.region === '华东')?.share, 10 / 14 * 100); assert.equal(analysis.rows.find(row => row.values.region === '华南')?.share, 4 / 14 * 100); assert.equal(analysis.rows.reduce((sum, row) => sum + row.count, 0), analysis.total); }); test('custom dimensions remove unknown, duplicate and inapplicable values without losing counts', () => { const analysis = getCustomAnalysis(cells, { view: 'operating', dimensions: ['region', 'bogus', 'region', 'abnormalAge', 'type'] as Parameters[1]['dimensions'], }); assert.deepEqual(analysis.dimensions, ['region', 'type']); assert.equal(analysis.rows.reduce((sum, row) => sum + row.count, 0), 14); for (const view of ['assets', 'operating', 'inventory', 'abnormal', 'pending', 'department', 'customer'] as const) { assert.equal(getDefaultDimensions(view).length, 1); assert.ok(getAvailableDimensions(view).includes(getDefaultDimensions(view)[0])); } }); test('custom sorting supports count order and semantic duration order', () => { const ascending = getCustomAnalysis(cells, { view: 'assets', dimensions: ['region', 'type'], sort: 'count-asc' }); assert.deepEqual(ascending.rows.map(row => row.count), [2, 4, 19]); const durationCells = ['unknown', '30d+', '0-3d'].map(abnormalAge => cell({ inventoryState: 'abnormal', abnormalAge: abnormalAge as AssetAnalysisCell['abnormalAge'] })); const byLabel = getCustomAnalysis(durationCells, { view: 'abnormal', dimensions: ['abnormalAge'], sort: 'label' }); assert.deepEqual(byLabel.rows.map(row => row.values.abnormalAge), ['0-3d', '30d+', 'unknown']); });