Files
ln-bi/src/server/routes/vehicles/repository.test.ts
T
2026-09-23 17:54:28 +08:00

164 lines
6.5 KiB
TypeScript

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import type { Vehicle, VehicleRow } from '../../types.js';
import {
FLOW_STATS_SQL,
MAIN_SQL,
VehicleRepository,
WEEKLY_DETAIL_SQL,
WEEKLY_TRUCK_ID_SQLS,
} from './repository.js';
class FakeDatabase {
readonly calls: Array<{ sql: string; values?: any[] }> = [];
constructor(private readonly responses: any[][]) {}
async query<T = any[]>(sql: string, values?: any[]): Promise<[T, ...any[]]> {
this.calls.push({ sql, values });
return [this.responses.shift() as T];
}
}
function vehicle(id: string): Vehicle {
return { id } as Vehicle;
}
test('keeps all vehicle GET endpoints in their established order', () => {
const source = readFileSync(new URL('./routes.ts', import.meta.url), 'utf8');
const paths = Array.from(
source.matchAll(/app\.get\('([^']+)'|app\.on\(\[([^\]]+)\],\s*'([^']+)'/g),
).flatMap((match) => match[1] ? [match[1]] : /'GET'/.test(match[2]) ? [match[3]] : []);
assert.deepEqual(paths, [
'/summary',
'/by-type',
'/by-batch',
'/inventory-analysis',
'/dept-stats',
'/region-stats',
'/customer-stats',
'/list',
'/inventory-stats',
'/weekly-detail',
'/flow-stats',
'/subjects',
'/refresh',
'/debug',
'/region-chart',
]);
assert.doesNotMatch(source, /\bSELECT\b|pool\.query|MAIN_SQL/);
});
test('keeps the non-empty vehicle cache for less than 5 seconds', async () => {
let now = 100_000;
const db = new FakeDatabase([[{ id: 'first' }], [{ id: 'second' }]]);
const repository = new VehicleRepository(
db,
() => now,
(row: VehicleRow) => vehicle(row.id),
);
assert.deepEqual(await repository.getVehicles(), [vehicle('first')]);
now += 4_999;
assert.deepEqual(await repository.getVehicles(), [vehicle('first')]);
now += 1;
assert.deepEqual(await repository.getVehicles(), [vehicle('second')]);
assert.deepEqual(db.calls.map((call) => call.sql), [MAIN_SQL, MAIN_SQL]);
});
test('keeps empty vehicle results uncached', async () => {
const db = new FakeDatabase([[], []]);
const repository = new VehicleRepository(db, () => 100_000);
await repository.getVehicles();
await repository.getVehicles();
assert.equal(db.calls.length, 2);
});
test('refresh invalidates vehicles but retains the weekly truck-id cache', async () => {
const db = new FakeDatabase([
[{ id: 'first' }],
[{ truck_id: 'pending' }],
[{ truck_id: 'delivered' }],
[{ truck_id: 'returned' }],
[{ truck_id: 'replaced' }, { truck_id: null }],
[{ id: 'second' }],
]);
const repository = new VehicleRepository(
db,
() => 100_000,
(row: VehicleRow) => vehicle(row.id),
);
await repository.getVehicles();
const weeklyBefore = await repository.getWeeklyTruckIds();
repository.invalidateRefreshCaches();
assert.deepEqual(await repository.getVehicles(), [vehicle('second')]);
assert.equal(await repository.getWeeklyTruckIds(), weeklyBefore);
assert.deepEqual(db.calls.map((call) => call.sql), [MAIN_SQL, ...WEEKLY_TRUCK_ID_SQLS, MAIN_SQL]);
assert.deepEqual([...weeklyBefore.replaced], ['replaced']);
});
test('keeps weekly detail SQL selection and flow date parameters unchanged', async () => {
const db = new FakeDatabase([[{ truck_id: '1' }], [{ id: 'delivered-1' }]]);
const repository = new VehicleRepository(db);
assert.deepEqual(await repository.getWeeklyDetailRows('new'), [{ truck_id: '1' }]);
assert.deepEqual(await repository.getFlowDetailRows('2026-08-01', '2026-08-13'), [{ id: 'delivered-1' }]);
assert.deepEqual(db.calls, [
{ sql: WEEKLY_DETAIL_SQL.new, values: undefined },
{
sql: FLOW_STATS_SQL,
values: ['2026-08-01', '2026-08-13', '2026-08-01', '2026-08-13'],
},
]);
});
test('flow dates and inclusive day boundaries use operations signing times, retaining creation time only for display', () => {
for (const [alias, field] of [['dv', 'delivery_completion_processed_time'], ['r', 'return_completion_processed_time']]) {
assert.ok(FLOW_STATS_SQL.includes(`DATE_FORMAT(${alias}.${field}, '%Y-%m-%d') AS stat_date`));
assert.ok(FLOW_STATS_SQL.includes(`AND ${alias}.${field} IS NOT NULL`));
assert.ok(FLOW_STATS_SQL.includes(`AND ${alias}.${field} >= ?`));
assert.ok(FLOW_STATS_SQL.includes(`AND ${alias}.${field} < DATE_ADD(?, INTERVAL 1 DAY)`));
assert.ok(FLOW_STATS_SQL.includes(`DATE_FORMAT(${alias}.create_time, '%Y-%m-%d %H:%i:%s') AS submit_time`));
}
assert.doesNotMatch(FLOW_STATS_SQL, /AND \w+\.create_time/);
assert.ok(FLOW_STATS_SQL.includes('ORDER BY flow.event_time DESC, flow.id DESC'));
});
test('flow retains multiple business events for the same vehicle', async () => {
const events = [
{ id: 'delivered-1', truck_id: '1', type: 'delivered', stat_date: '2026-09-16' },
{ id: 'delivered-2', truck_id: '1', type: 'delivered', stat_date: '2026-09-16' },
{ id: 'returned-1', truck_id: '1', type: 'returned', stat_date: '2026-09-16' },
];
const repository = new VehicleRepository(new FakeDatabase([events]));
assert.deepEqual(await repository.getFlowDetailRows('2026-09-16', '2026-09-16'), events);
});
test('replacement trips come exclusively from linked delivery records', () => {
assert.ok(FLOW_STATS_SQL.includes("CASE WHEN dv.vehicle_replacement_id IS NOT NULL THEN 'replaced' ELSE 'delivered' END AS type"));
assert.ok(FLOW_STATS_SQL.includes("CASE WHEN dv.vehicle_replacement_id IS NOT NULL THEN '替换交车' ELSE '交车' END AS type_label"));
assert.equal((FLOW_STATS_SQL.match(/FROM delivery_vehicle/g) || []).length, 1);
assert.doesNotMatch(FLOW_STATS_SQL, /FROM vehicle_replacement|dv\.delivery_time|r\.arrival_time|vr\.replace_time/);
});
test('delivery and replacement trips include awaiting customer signature and exclude suspended subjects', () => {
const [deliveries, returns] = FLOW_STATS_SQL.split('UNION ALL');
assert.ok(deliveries.includes('AND dv.delivery_status IN (2,3,5,20)'));
assert.ok(deliveries.includes('AND (dts.status IS NULL OR dts.status <> 10)'));
assert.doesNotMatch(deliveries, /dts\.status\s*(?:=|IN)|dv\.delivery_status IN \(2,3,5\)/);
assert.ok(returns.includes('AND r.status IN (2,3,5)'));
});
test('return history includes completed, signed and awaiting-signature statuses without arrival or settlement requirements', () => {
const returns = FLOW_STATS_SQL.split('UNION ALL')[1];
assert.ok(returns.includes('AND r.status IN (2,3,5)'));
assert.ok(returns.includes('AND r.return_completion_processed_time >= ?'));
assert.doesNotMatch(returns, /r\.is_arrived|r\.arrival_time|settlement|AND dts\.status|AND \(dts\.status/);
});