fix(dkv): fix Kennzeichen matching and NaN/invalid km values in Excel export
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 38s
Tessera CI/CD / Build & Publish Images (push) Successful in 23s

Three issues fixed:

1. Kennzeichen normalization: DKV PDF extracts plates without hyphens
   ("GP JL 740E" vs CSV-imported "GP-JL 740E"). Added _normalizeKennzeichen()
   which strips hyphens, spaces, and dots before lookup — resolves vehicle
   master match failure that caused Marke/Modell/Fahrer to appear empty.

2. Empty-string NaN: parser used ?? '0' which doesn't catch empty strings,
   causing parseDE('') = NaN. Changed to || '0' for km, menge, and totals.

3. Invalid km values: EV charging rows from DKV have misaligned columns —
   km position contains a decimal price (e.g. 18.64 EUR or kWh). Added
   sanity check: non-integer km values are written as null (empty cell)
   instead of a misleading decimal. ExportRow.kilometerstand is now number|null.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 09:12:13 +02:00
parent c9328f60b7
commit a759e816a0
3 changed files with 31 additions and 12 deletions
+23 -5
View File
@@ -579,15 +579,16 @@ export class DkvService {
tenantId: string,
vehicles: DkvVehicleBlock[],
vehicleFormatString: string,
): Promise<{ lieferdatum: string; fahrzeug: string; fahrer: string; ort: string; kilometerstand: number }[]> {
): Promise<{ lieferdatum: string; fahrzeug: string; fahrer: string; ort: string; kilometerstand: number | null }[]> {
// Batch load vehicle master to avoid N+1 queries
const masters = await this.prisma.dkvVehicleMaster.findMany({ where: { tenantId } });
const masterMap = new Map(masters.map((m) => [m.kennzeichen, m]));
// Normalize keys: DKV PDF may omit hyphens or use spaces ("GP JL 740E" vs "GP-JL 740E")
const masterMap = new Map(masters.map((m) => [_normalizeKennzeichen(m.kennzeichen), m]));
const rows: { lieferdatum: string; fahrzeug: string; fahrer: string; ort: string; kilometerstand: number }[] = [];
const rows: { lieferdatum: string; fahrzeug: string; fahrer: string; ort: string; kilometerstand: number | null }[] = [];
for (const vehicle of vehicles) {
const master = masterMap.get(vehicle.kennzeichen);
const master = masterMap.get(_normalizeKennzeichen(vehicle.kennzeichen));
// For unknown plates: use empty string values for unknown fields
const vehicleForFormat = {
@@ -601,12 +602,20 @@ export class DkvService {
const fahrer = master?.fahrer ?? '';
for (const tx of vehicle.transactions) {
// Only keep km values that are plausible odometer readings (whole numbers ≥ 0).
// Decimal values indicate a misaligned column (e.g. kWh or EUR from EV charging rows).
const rawKm = tx.kilometerstand;
const km: number | null =
Number.isNaN(rawKm) || !Number.isFinite(rawKm) || !Number.isInteger(rawKm)
? null
: rawKm;
rows.push({
lieferdatum: tx.lieferdatum,
fahrzeug,
fahrer,
ort: tx.ort,
kilometerstand: tx.kilometerstand,
kilometerstand: km,
});
}
}
@@ -684,3 +693,12 @@ function _parseVehicleCsv(
function _delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Normalize a Kennzeichen for fuzzy vehicle lookup.
* DKV PDF extraction may omit hyphens or use spaces as separators.
* Example: "GP JL 740E" == "GP-JL 740E" == "GPJL740E" after normalization.
*/
function _normalizeKennzeichen(k: string): string {
return k.toUpperCase().replace(/[\s\-\.]/g, '');
}