diff --git a/apps/api/src/dkv/dkv-parser.service.ts b/apps/api/src/dkv/dkv-parser.service.ts index 6d1f67e..a847748 100644 --- a/apps/api/src/dkv/dkv-parser.service.ts +++ b/apps/api/src/dkv/dkv-parser.service.ts @@ -69,17 +69,15 @@ export class DkvParserService { return m ? m[1] : null; } - /** Extract invoice date ("DD.MM.YYYY") appearing near "Rechnungsdatum" in PDF text. */ + /** Extract invoice date ("DD.MM.YYYY") from PDF text. + * DKV PDFs always print the date on the line immediately after the invoice + * number (DD/DDDDDDDDD/DDD), so we anchor on the number → date pairing. + * The old "Rechnungsdatum[:\s]*date" approach picked up the payment-due date + * ("10 Tage nach Rechnungsdatum. Die Abbuchung erfolgt am: 10.04.2026"). + */ private extractRechnungsdatum(text: string): string | null { - // "Rechnungsdatum" is followed (within ~200 chars) by a date string - const m = text.match(/Rechnungsdatum[:\s\t]*(\d{2}\.\d{2}\.\d{4})/); - if (m) return m[1]; - // Fallback: second line after "Rechnungsdatum:" marker - const idx = text.indexOf('Rechnungsdatum'); - if (idx === -1) return null; - const after = text.slice(idx, idx + 200); - const dm = after.match(/(\d{2}\.\d{2}\.\d{4})/); - return dm ? dm[1] : null; + const m = text.match(/\d{2}\/\d{9}\/\d{3}\s+(\d{2}\.\d{2}\.\d{4})/); + return m ? m[1] : null; } // ─── Private: PDF text extraction ────────────────────────────────────────── diff --git a/apps/api/src/dkv/providers/exchange-inbox.provider.ts b/apps/api/src/dkv/providers/exchange-inbox.provider.ts index 03c6676..7dd4e99 100644 --- a/apps/api/src/dkv/providers/exchange-inbox.provider.ts +++ b/apps/api/src/dkv/providers/exchange-inbox.provider.ts @@ -24,14 +24,22 @@ function soapEnvelope(body: string): string { } function findItemSoap(folderId: string, maxResults: number, senderFilter?: string): string { + const isReadFilter = ` + + + `; + const restriction = senderFilter ? ` - - - - + + ${isReadFilter} + + + + + ` - : ''; + : `${isReadFilter}`; return soapEnvelope(` @@ -71,6 +79,23 @@ function getItemSoap(itemIds: string[]): string { `); } +function markReadSoap(itemId: string, changeKey: string): string { + return soapEnvelope(` + + + + + + + + true + + + + + `); +} + function getAttachmentSoap(attachmentId: string): string { return soapEnvelope(` @@ -259,6 +284,7 @@ export class ExchangeInboxProvider implements InboxProvider { for (const block of messageBlocks) { const uid = extractAttr(block, 't:ItemId', 'Id'); + const changeKey = extractAttr(block, 't:ItemId', 'ChangeKey'); const subject = extractAll(block, 't:Subject')[0] ?? ''; const messageId = extractAll(block, 't:InternetMessageId')[0] ?? ''; const from = (extractAttr(block, 't:Mailbox', 'SmtpAddress') || @@ -317,6 +343,15 @@ export class ExchangeInboxProvider implements InboxProvider { if (attachments.length === 0) continue; results.push({ uid, messageId, subject, from, date, attachments }); + + // Mark as read so subsequent polls skip this message (mirrors IMAP \Seen flag). + if (uid && changeKey) { + try { + await this.ewsPost(config, markReadSoap(uid, changeKey), 'UpdateItem'); + } catch { + // Non-fatal: message will reappear on next poll but IsRead filter will catch it + } + } } }