diff --git a/src/modules/energy/HydrogenDaily.tsx b/src/modules/energy/HydrogenDaily.tsx index 1642789..c4ceefa 100644 --- a/src/modules/energy/HydrogenDaily.tsx +++ b/src/modules/energy/HydrogenDaily.tsx @@ -58,6 +58,7 @@ export default function HydrogenDaily() { ? drillContext.customerName : undefined; const selectedDate = drillContext.selectedDate; + const selectedDailyRow = rows?.find(row => row.date === selectedDate); const commitDrillContext = useCallback((next: HydrogenDrillContext, mode: 'push' | 'replace') => { const url = buildHydrogenDrillUrl(window.location, next); @@ -386,6 +387,7 @@ export default function HydrogenDaily() { {selectedDate && ( @@ -74,11 +87,32 @@ export default function HydrogenOrders({ {data ? ( -
-
订单 / 加氢量
{data.recordCount} 笔 · {data.totalKg.toLocaleString('zh-CN')} Kg
-
采购成本
{fmtMoney(data.totalCost)}
-
客户收入
{fmtMoney(data.totalRevenue)}
-
+ <> +
+
订单 / 加氢量
{data.recordCount} 笔 · {data.totalKg.toLocaleString('zh-CN')} Kg
+
采购成本
{fmtMoney(data.totalCost)}
+
客户收入
{fmtMoney(data.totalRevenue)}
+
+ {reconciliation && ( +
+ {reconciliation.matches + ? + : } + + {reconciliation.matches + ? `口径核对通过:日汇总与 ${data.recordCount} 笔订单全量加氢量一致` + : `口径存在差异:加氢量 ${reconciliation.kgDifference >= 0 ? '+' : ''}${reconciliation.kgDifference.toFixed(2)} Kg${grainDifferences ? ` · ${grainDifferences}不一致` : ''}`} + +
+ )} + ) : null} {error ? ( diff --git a/src/modules/energy/hydrogen-reconciliation.test.ts b/src/modules/energy/hydrogen-reconciliation.test.ts new file mode 100644 index 0000000..b039d75 --- /dev/null +++ b/src/modules/energy/hydrogen-reconciliation.test.ts @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { reconcileHydrogenDaily } from './hydrogen-reconciliation.js'; +import type { HydrogenDailyRow, HydrogenOrderResponse } from './types.js'; + +const daily: HydrogenDailyRow = { + date: '2026-07-01', + totalKg: 123.45, + chainPct: 0, + customerType: 'lingniu', + stations: [], +}; + +const orders: HydrogenOrderResponse = { + date: '2026-07-01', + vehicleScope: 'lingniu', + stationId: 18, + customerName: null, + page: 1, + limit: 20, + recordCount: 3, + totalPages: 1, + totalKg: 123.45, + totalCost: 1000, + totalRevenue: 1200, + items: [], +}; + +test('matches daily hydrogen totals at the same date, scope, and entity grain', () => { + assert.deepEqual(reconcileHydrogenDaily(daily, orders, 'lingniu', 18), { + matches: true, + dateMatches: true, + scopeMatches: true, + entityMatches: true, + kgDifference: 0, + }); +}); + +test('reports amount and filter-grain differences independently', () => { + const result = reconcileHydrogenDaily( + daily, + { + ...orders, + date: '2026-07-02', + vehicleScope: 'external', + stationId: null, + customerName: '武汉客户', + totalKg: 123.44, + }, + 'lingniu', + 18, + ); + + assert.equal(result.matches, false); + assert.equal(result.dateMatches, false); + assert.equal(result.scopeMatches, false); + assert.equal(result.entityMatches, false); + assert.equal(result.kgDifference, -0.01); +}); + +test('matches an exact customer drill without requiring a station', () => { + const customerOrders = { ...orders, stationId: null, customerName: '武汉客户' }; + assert.equal( + reconcileHydrogenDaily(daily, customerOrders, 'lingniu', undefined, '武汉客户').matches, + true, + ); +}); diff --git a/src/modules/energy/hydrogen-reconciliation.ts b/src/modules/energy/hydrogen-reconciliation.ts new file mode 100644 index 0000000..83a2607 --- /dev/null +++ b/src/modules/energy/hydrogen-reconciliation.ts @@ -0,0 +1,35 @@ +import type { CustomerType, HydrogenDailyRow, HydrogenOrderResponse } from './types'; + +export interface HydrogenDailyReconciliation { + matches: boolean; + dateMatches: boolean; + scopeMatches: boolean; + entityMatches: boolean; + kgDifference: number; +} + +function hundredths(value: number): number { + return Math.round(value * 100); +} + +export function reconcileHydrogenDaily( + daily: HydrogenDailyRow, + orders: HydrogenOrderResponse, + expectedScope: CustomerType, + expectedStationId?: number, + expectedCustomerName?: string, +): HydrogenDailyReconciliation { + const dateMatches = daily.date === orders.date; + const scopeMatches = expectedScope === orders.vehicleScope; + const entityMatches = (expectedStationId ?? null) === orders.stationId + && (expectedCustomerName ?? null) === orders.customerName; + const kgDifferenceInHundredths = hundredths(orders.totalKg) - hundredths(daily.totalKg); + + return { + matches: dateMatches && scopeMatches && entityMatches && kgDifferenceInHundredths === 0, + dateMatches, + scopeMatches, + entityMatches, + kgDifference: kgDifferenceInHundredths / 100, + }; +}