refactor: modularize application domains
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import { normalizeOneOsProtocol, type OneOsProtocol } from './source-policy.js';
|
||||
|
||||
export interface OneOsDailyMileage {
|
||||
vin: string;
|
||||
plateNumber: string;
|
||||
date: string;
|
||||
dailyMileageKm: number | null;
|
||||
totalMileageKm: number | null;
|
||||
status: 'NORMAL' | 'NO_DATA';
|
||||
dataTime: string | null;
|
||||
calculatedAt: string | null;
|
||||
updatedAt: string | null;
|
||||
sourceProtocol: OneOsProtocol | null;
|
||||
}
|
||||
|
||||
const MILEAGE_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
export function assertMileageDate(date: string): void {
|
||||
if (!MILEAGE_DATE_PATTERN.test(date)) throw new Error(`Invalid mileage date: ${date}`);
|
||||
}
|
||||
|
||||
function stringField(row: Record<string, unknown>, ...names: string[]): string | null {
|
||||
for (const name of names) {
|
||||
const value = row[name];
|
||||
if (typeof value === 'string' && value.trim()) return value.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function nonNegativeMileage(value: unknown, status: OneOsDailyMileage['status']): number | null {
|
||||
if (status !== 'NORMAL' || value === null || value === undefined) return null;
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? Math.max(0, numeric) : null;
|
||||
}
|
||||
|
||||
/** Normalize the provider response and retain the highest mileage row per plate. */
|
||||
export function normalizeOneOsRows(value: unknown, requestedDate: string): OneOsDailyMileage[] {
|
||||
if (!Array.isArray(value)) throw new Error('OneOS mileage API returned a non-array data field');
|
||||
|
||||
const best = new Map<string, OneOsDailyMileage>();
|
||||
for (const item of value) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
const row = item as Record<string, unknown>;
|
||||
const vin = typeof row.vin === 'string' ? row.vin.trim() : '';
|
||||
const plateNumber = typeof row.plateNumber === 'string' ? row.plateNumber.trim() : '';
|
||||
const date = typeof row.date === 'string' ? row.date : requestedDate;
|
||||
const status = row.status === 'NORMAL' ? 'NORMAL' : 'NO_DATA';
|
||||
|
||||
// The aliases keep the BI compatible while the Open API formalizes its fields.
|
||||
const dataTime = stringField(row, 'dataTime', 'statisticTime', 'recordTime');
|
||||
const calculatedAt = stringField(row, 'calculatedAt', 'calculationTime');
|
||||
const updatedAt = stringField(row, 'updatedAt');
|
||||
const sourceProtocol = normalizeOneOsProtocol(
|
||||
stringField(row, 'sourceProtocol', 'protocol', 'dataSource'),
|
||||
);
|
||||
if (!plateNumber || date !== requestedDate) continue;
|
||||
|
||||
const normalized: OneOsDailyMileage = {
|
||||
vin,
|
||||
plateNumber,
|
||||
date,
|
||||
dailyMileageKm: nonNegativeMileage(row.dailyMileageKm, status),
|
||||
totalMileageKm: nonNegativeMileage(row.totalMileageKm, status),
|
||||
status,
|
||||
dataTime,
|
||||
calculatedAt,
|
||||
updatedAt,
|
||||
sourceProtocol,
|
||||
};
|
||||
const existing = best.get(plateNumber);
|
||||
if (!existing || (normalized.dailyMileageKm ?? -1) > (existing.dailyMileageKm ?? -1)) {
|
||||
best.set(plateNumber, normalized);
|
||||
}
|
||||
}
|
||||
return Array.from(best.values());
|
||||
}
|
||||
|
||||
export function normalizeOneOsRangeRows(
|
||||
value: unknown,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
): Map<string, OneOsDailyMileage[]> {
|
||||
if (!Array.isArray(value)) throw new Error('OneOS mileage range API returned a non-array data field');
|
||||
const rawByDate = new Map<string, unknown[]>();
|
||||
for (const item of value) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
const date = (item as Record<string, unknown>).date;
|
||||
if (typeof date !== 'string' || date < startDate || date > endDate) continue;
|
||||
const rows = rawByDate.get(date) || [];
|
||||
rows.push(item);
|
||||
rawByDate.set(date, rows);
|
||||
}
|
||||
|
||||
const result = new Map<string, OneOsDailyMileage[]>();
|
||||
for (const [date, rows] of rawByDate) result.set(date, normalizeOneOsRows(rows, date));
|
||||
return result;
|
||||
}
|
||||
|
||||
export function normalizeSelectedPlates(plateNumbers?: string[]): string[] {
|
||||
return Array.from(new Set(
|
||||
(plateNumbers || []).map(plate => plate.trim()).filter(Boolean),
|
||||
)).sort();
|
||||
}
|
||||
|
||||
export function groupContiguousMileageDates(dates: string[], maxGroupSize = 366): string[][] {
|
||||
const groups: string[][] = [];
|
||||
for (const date of Array.from(new Set(dates)).sort()) {
|
||||
assertMileageDate(date);
|
||||
const current = groups[groups.length - 1];
|
||||
if (!current) {
|
||||
groups.push([date]);
|
||||
continue;
|
||||
}
|
||||
const nextDate = new Date(`${current[current.length - 1]}T00:00:00Z`);
|
||||
nextDate.setUTCDate(nextDate.getUTCDate() + 1);
|
||||
if (nextDate.toISOString().slice(0, 10) === date && current.length < maxGroupSize) {
|
||||
current.push(date);
|
||||
} else {
|
||||
groups.push([date]);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
export function oneOsRangeRequestKey(
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
selectedPlates: string[],
|
||||
protocolPriority: OneOsProtocol[],
|
||||
): string {
|
||||
return [
|
||||
startDate,
|
||||
endDate,
|
||||
selectedPlates.length > 0 ? selectedPlates.join('\u0000') : '*',
|
||||
protocolPriority.join('>'),
|
||||
].join(':');
|
||||
}
|
||||
Reference in New Issue
Block a user