fix(dkv): handle EV charging rows and service rows in single-tx tab parser
Analyzed Invoice-4302486921-26_650869002_000.pdf text structure. Three cases: 1. FUEL (fields[4] = numeric tx-nr): km+product merged in fields[5], unit in fields[6]. Already working; no change. 2. EV CHARGING (fields contains "DDDD KWH" or "DDDD MIN" unit): column layout shifts — no km field, station+ort sometimes merged in fields[1]. Detected by regex on unit field; kwhIdx drives relative offset for menge/netto/brutto. Ort extracted from fields[2] (kwhIdx>=5) or fields[1] (kwhIdx=4, compact). Kilometerstand = 0 (EV chargers don't record odometer). 3. SERVICE ROWS (e.g. "DKV Analytics Premiu"): appear inside a VEHICLE: block but fields[4] is non-numeric (product description, not a transaction number). These were being parsed as fake vehicle transactions producing wrong ort/km values. Now filtered out (return null). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user