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
+6 -5
View File
@@ -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'),
}));
}
}
+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, '');
}
+2 -2
View File
@@ -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;
}