diff --git a/apps/api/src/dkv/dkv-parser.service.ts b/apps/api/src/dkv/dkv-parser.service.ts index f6dddd6..012ea78 100644 --- a/apps/api/src/dkv/dkv-parser.service.ts +++ b/apps/api/src/dkv/dkv-parser.service.ts @@ -115,21 +115,31 @@ export class DkvParserService { // ─── Private: Single-transaction tab-separated line ──────────────────────── // - // DKV single-tx tab format (empirically verified against invoice.pdf): + // DKV produces two distinct single-tx tab formats: // + // FUEL format: // [0] Lieferdatum DD.MM.YYYY // [1] Station Name e.g. "ESSO" // [2] Ort e.g. "DONZDORF" // [3] Servicest.-Nr e.g. "0071132" - // [4] Transaktions-Nr e.g. "189945" - // [5] "KM PRODUKT" e.g. "68040 DIESEL" (km and product merged, no tab between) + // [4] Transaktions-Nr e.g. "189945" ← purely numeric + // [5] "KM PRODUKT" e.g. "68040 DIESEL" (km and product merged) + // or just PRODUKT when driver did not enter km // [6] "CODE UNIT" e.g. "0009 LTR" // [7] Menge e.g. "45,910" - // [8] Preis/E brutto e.g. "2,1790" - // [9] Preis/E netto e.g. "1,8312" - // [10] Bezugswert brutto e.g. "100,04" - // [11] Bezugswert netto e.g. "84,07" - // ... + // [10] Bezugswert brutto + // [11] Bezugswert netto + // + // EV CHARGING format — detected by "DDDD KWH" unit field: + // kwhIdx=5 (standard EV, station and ort separate): + // [1] Station Name, [2] Ort, [3] Charge Point ID, + // [4] Produkt, [5] "0964 KWH", [6] Menge, [9] netto, [12] brutto + // kwhIdx=4 (compact EV, station+ort merged into [1]): + // [1] "Name ORT" merged, [2] Charge Point ID, + // [3] Produkt, [4] "0963 KWH", [5] Menge, [8] netto, [11] brutto + // + // SERVICE ROWS (e.g. "DKV Analytics Premiu") appear inside a VEHICLE: block but + // are not vehicle transactions — detected by non-numeric fields[4] → skip. private parseSingleTxLine(line: string): DkvTransaction | null { const fields = line.split('\t').map(f => f.trim()); @@ -138,7 +148,31 @@ export class DkvParserService { const datePattern = /^\d{2}\.\d{2}\.\d{4}$/; if (!datePattern.test(fields[0])) return null; - // fields[5] = "KM_VALUE PRODUKT" — split on first whitespace boundary + // Detect EV charging: look for a unit field matching "DDDD KWH" or "DDDD MIN" + const kwhIdx = fields.findIndex(f => /^\d{4}\s+(KWH|MIN)$/i.test(f)); + if (kwhIdx !== -1) { + // kwhIdx=5: ort is separate in fields[2]; kwhIdx=4: station+ort merged in fields[1] + const ort = kwhIdx >= 5 ? (fields[2] ?? '') : (fields[1] ?? ''); + const mengeIdx = kwhIdx + 1; + const nettoIdx = kwhIdx + 3; + const bruttoIdx = kwhIdx + 6; + return { + lieferdatum: fields[0], + ort, + kilometerstand: 0, + produkt: fields[kwhIdx - 1] ?? '', + menge: parseDE(fields[mengeIdx] || '0'), + einheit: fields[kwhIdx]?.split(/\s+/)[1] ?? 'KWH', + netto: parseDE(fields[nettoIdx] || '0'), + brutto: parseDE(fields[bruttoIdx] || '0'), + }; + } + + // Standard fuel rows: fields[4] must be a numeric transaction number. + // Non-numeric field[4] = service charge row (e.g. DKV Analytics fee) → skip. + if (!/^\d+$/.test(fields[4] ?? '')) return null; + + // fields[5] = "KM PRODUKT" merged, or just PRODUKT when driver skipped km entry const kmAndProdukt = fields[5].split(/\s+/); const km = kmAndProdukt[0] ?? '0'; const produkt = kmAndProdukt.slice(1).join(' '); @@ -152,10 +186,10 @@ export class DkvParserService { ort: fields[2] ?? '', kilometerstand: parseDE(km), produkt, - menge: parseDE(fields[7] ?? '0'), + menge: parseDE(fields[7] || '0'), einheit, - netto: parseDE(fields[11] ?? '0'), - brutto: parseDE(fields[10] ?? '0'), + netto: parseDE(fields[11] || '0'), + brutto: parseDE(fields[10] || '0'), }; }