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
+ }
+ }
}
}