From a759e816a0fa9d8aa314ce10e3783b759acbe35f Mon Sep 17 00:00:00 2001 From: Schalli Date: Tue, 30 Jun 2026 09:12:13 +0200 Subject: [PATCH] fix(dkv): fix Kennzeichen matching and NaN/invalid km values in Excel export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/api/src/dkv/dkv-parser.service.ts | 11 +++++----- apps/api/src/dkv/dkv.service.ts | 28 +++++++++++++++++++++----- apps/api/src/dkv/dkv.types.ts | 4 ++-- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/apps/api/src/dkv/dkv-parser.service.ts b/apps/api/src/dkv/dkv-parser.service.ts index f9237c8..f6dddd6 100644 --- a/apps/api/src/dkv/dkv-parser.service.ts +++ b/apps/api/src/dkv/dkv-parser.service.ts @@ -207,13 +207,14 @@ export class DkvParserService { return dates.map((date, i) => ({ lieferdatum: date, ort: orts[i] ?? '', - // German number format: "19.234,56" → 19234.56 (strip dots, replace comma) - kilometerstand: parseDE(kms[i] ?? '0'), + // German number format: "19.234,56" → 19234.56 (strip dots, replace comma). + // Use || '0' (not ??) so empty strings also fall back to '0' (avoids NaN). + kilometerstand: parseDE(kms[i] || '0'), produkt: products[i] ?? '', - menge: parseDE(quantities[i] ?? '0'), + menge: parseDE(quantities[i] || '0'), einheit: units[i] ?? '', - netto: parseDE(totals_netto[i] ?? '0'), - brutto: parseDE(totals_brutto[i] ?? '0'), + netto: parseDE(totals_netto[i] || '0'), + brutto: parseDE(totals_brutto[i] || '0'), })); } } diff --git a/apps/api/src/dkv/dkv.service.ts b/apps/api/src/dkv/dkv.service.ts index 8c821fc..4ac77f5 100644 --- a/apps/api/src/dkv/dkv.service.ts +++ b/apps/api/src/dkv/dkv.service.ts @@ -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 { 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, ''); +} diff --git a/apps/api/src/dkv/dkv.types.ts b/apps/api/src/dkv/dkv.types.ts index 1e58c47..2560d95 100644 --- a/apps/api/src/dkv/dkv.types.ts +++ b/apps/api/src/dkv/dkv.types.ts @@ -98,6 +98,6 @@ export interface ExportRow { fahrer: string; /** Service station city from PDF */ ort: string; - /** Numeric odometer reading */ - kilometerstand: number; + /** Numeric odometer reading; null when not available (e.g. EV charging rows) */ + kilometerstand: number | null; }