fix(dkv): extract invoice number/date from PDF, rename export files to RG-DKV format
- Parser now extracts Rechnungsnummer (DD/DDDDDDDDD/DDD) and Rechnungsdatum
from PDF text, so filename doesn't rely on email subject
- Export filename changed from DKV_YYYY-MM_... to RG-DKV-{nr}-{YYMMDD}.xlsx
e.g. RG-DKV-26-650869002-002-260331.xlsx
- Subject fallback now also matches slash-separated invoice numbers (26/NNN/NNN)
- writeAndPrune simplified to accept baseName instead of separate fields
- Validation regex and prune prefix updated to match new RG-DKV- pattern
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -342,12 +342,12 @@ export class DkvService {
|
||||
vehicleFormatString: string,
|
||||
): Promise<void> {
|
||||
// D-10: Up to 3 parse retries
|
||||
let vehicles: DkvVehicleBlock[] | null = null;
|
||||
let parseResult: Awaited<ReturnType<typeof this.parser.parsePdf>> | null = null;
|
||||
let parseError: string | null = null;
|
||||
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
vehicles = await this.parser.parsePdf(pdfBuffer);
|
||||
parseResult = await this.parser.parsePdf(pdfBuffer);
|
||||
parseError = null;
|
||||
break;
|
||||
} catch (err) {
|
||||
@@ -358,12 +358,12 @@ export class DkvService {
|
||||
}
|
||||
}
|
||||
|
||||
if (!vehicles) {
|
||||
if (!parseResult) {
|
||||
// Record parse failure in history (D-10)
|
||||
await this.prisma.dkvInvoiceHistory.create({
|
||||
data: {
|
||||
tenantId,
|
||||
rechnungsnummer: this._extractInvoiceNumber(email.subject, email.uid),
|
||||
rechnungsnummer: this._buildRechnungsnummer(null, email.subject, email.uid),
|
||||
anzahlFahrzeuge: 0,
|
||||
anzahlTransaktionen: 0,
|
||||
status: 'Fehler',
|
||||
@@ -373,9 +373,13 @@ export class DkvService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract invoice metadata for filename + history record
|
||||
const rechnungsnummer = this._extractInvoiceNumber(email.subject, email.uid);
|
||||
const invoiceMonth = this._extractInvoiceMonth(vehicles);
|
||||
const { vehicles, rechnungsnummer: rgNr, rechnungsdatum: rgDat } = parseResult;
|
||||
|
||||
// Build filename: RG-DKV-{nr}-{date}.xlsx
|
||||
const rechnungsnummer = this._buildRechnungsnummer(rgNr, email.subject, email.uid);
|
||||
const datePart = rgDat ? _formatDateYYMMDD(rgDat) : this._extractInvoiceMonth(vehicles).replace('-', '');
|
||||
const exportBaseName = `RG-DKV-${rechnungsnummer}-${datePart}`;
|
||||
|
||||
const anzahlTransaktionen = vehicles.reduce((s, v) => s + v.transactions.length, 0);
|
||||
|
||||
// Build export rows — look up drivers from vehicle master
|
||||
@@ -383,7 +387,7 @@ export class DkvService {
|
||||
|
||||
// Generate xlsx buffer and write to user-files/ (DkvExportService)
|
||||
const xlsxBuffer = this.exporter.buildExcelBuffer(exportRows);
|
||||
const exportFilename = this.exporter.writeAndPrune(xlsxBuffer, rechnungsnummer, invoiceMonth);
|
||||
const exportFilename = this.exporter.writeAndPrune(xlsxBuffer, exportBaseName);
|
||||
|
||||
// D-16: SMTP send with 3-retry exponential backoff
|
||||
let smtpStatus: 'Verarbeitet' | 'Versand fehlgeschlagen' = 'Verarbeitet';
|
||||
@@ -552,7 +556,7 @@ export class DkvService {
|
||||
filename.includes('/') ||
|
||||
filename.includes('\\') ||
|
||||
filename.includes('..') ||
|
||||
!/^DKV_[\w\-]+\.xlsx$/.test(filename)
|
||||
!/^(RG-DKV-|DKV_)[\w\-]+\.xlsx$/.test(filename)
|
||||
) {
|
||||
throw new BadRequestException('Invalid export filename');
|
||||
}
|
||||
@@ -624,17 +628,19 @@ export class DkvService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a DKV invoice number from the email subject line.
|
||||
* Pattern: DD-DDDDDDDDD-DDD (e.g. "26-651566449-001")
|
||||
* Falls back to "email-{uid}" when no match is found.
|
||||
* Build a safe rechnungsnummer string for use in filenames.
|
||||
* Priority: PDF-extracted number → email subject → uid fallback.
|
||||
* All slashes and non-safe chars are replaced so the value is path-safe.
|
||||
*/
|
||||
private _extractInvoiceNumber(subject: string, uid: number | string): string {
|
||||
const match = subject?.match(/(\d{2}-\d{9}-\d{3})/);
|
||||
if (match?.[1]) return match[1];
|
||||
// Fallback when invoice number cannot be parsed from subject.
|
||||
// Exchange UniqueIds are base64 and can contain '+', '/', '=' which would
|
||||
// introduce path separators into the generated filename (WR-04).
|
||||
// Sanitise to [a-zA-Z0-9-] before the value reaches the filesystem.
|
||||
private _buildRechnungsnummer(
|
||||
fromPdf: string | null,
|
||||
subject: string,
|
||||
uid: number | string,
|
||||
): string {
|
||||
if (fromPdf) return fromPdf.replace(/\//g, '-');
|
||||
// DKV email subjects carry the invoice number with slashes: "26/650869002/002"
|
||||
const m = subject?.match(/(\d{2}[\/\-]\d{9}[\/\-]\d{3})/);
|
||||
if (m?.[1]) return m[1].replace(/\//g, '-');
|
||||
return `email-${String(uid).replace(/[^a-zA-Z0-9\-]/g, '_')}`;
|
||||
}
|
||||
|
||||
@@ -694,6 +700,13 @@ function _delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/** Format "DD.MM.YYYY" → "YYMMDD" for compact filename date segment. */
|
||||
function _formatDateYYMMDD(date: string): string {
|
||||
const [d, m, y] = date.split('.');
|
||||
if (!d || !m || !y) return date.replace(/\./g, '');
|
||||
return `${y.slice(2)}${m}${d}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a Kennzeichen for fuzzy vehicle lookup.
|
||||
* DKV PDF extraction may omit hyphens or use spaces as separators.
|
||||
|
||||
Reference in New Issue
Block a user