Files
tessera-ctl/.planning/phases/14-rss-email-alert-ingestion-module-rollout/14-CONTEXT.md
T

13 KiB

Phase 14: RSS, Email-Alert Ingestion & Module Rollout - Context

Gathered: 2026-07-23 Status: Ready for planning

## Phase Boundary

Phase 14 rundet die Ausschreibungs-Abdeckung im Unterschwellenbereich ab und macht das Modul auslieferungsreif. Vier Fähigkeiten:

  1. RSS-Ingestion — öffentliche Feeds (subreport-elvis, service.bund.de) werden automatisch als zusätzliche Ausschreibungsquelle eingelesen (INGEST-04).
  2. E-Mail-Alert-Ingestion — Tessera liest ein pro Mandant konfiguriertes Postfach aus, in das Vergabeportale ihre Benachrichtigungsmails schicken (eingehend, nicht ausgehend), und macht daraus Ausschreibungs-Einträge. Baut auf einem geteilten inbox/-Modul, das als einmaliger Prerequisite-Refactor aus DKV herausgelöst wird (INGEST-05).
  3. Admin-Konfiguration — pro Mandant Quellen-Poll- und Postfach-Konfiguration, Zugangsdaten AES-256-GCM-verschlüsselt (CONFIG-02, CONFIG-03).
  4. Denylist-Transparenz — vergabe24 und aumass werden im UI als „manuell beobachten" mit Direktlink gezeigt statt als stille Lücke (UI-06).
  5. i18n — das gesamte Modul-UI liegt in Deutsch und Englisch vor (Teil von UI-06/Modul-Rollout).

Wichtige Klarstellung (aus Diskussion): Kriterium 2 ist eingehende E-Mail (Tessera liest ein Postfach), nicht ausgehende (das Versenden von Nutzer-Benachrichtigungen ist bereits in Phase 12 gebaut, via SMTP). Das Ausschreibungs-Postfach ist ein separates Postfach mit eigener Adresse und eigenen Zugangsdaten — NICHT dasselbe, an das DKV-Rechnungen gehen.

## Implementation Decisions

Inbox-Modul-Extraktion (INGEST-05, Refactor)

  • D-01: Additiver Refactor, DKV bleibt verhaltensgleich. Die bestehenden IMAP-/Exchange-Provider (ImapProvider, ExchangeInboxProvider) und das InboxProvider-Interface werden aus apps/api/src/dkv/providers/ in ein geteiltes apps/api/src/inbox/-Modul gehoben. Geteilt wird ausschließlich die Verbindungsmechanik (IMAP/Exchange-Anmeldung, TLS, NTLM via httpntlm, Timeouts, Mail-Abruf).
  • D-02: Die bestehende PDF-Anhang-Methode (fetchPdfAttachments) bleibt unverändert erhalten. Für die Ausschreibungs-Ingestion wird eine zusätzliche Methode ergänzt, die den Mail-Body (HTML/Text) und ganze Nachrichten liefert. DKV wird NICHT auf die neue Methode umgestellt — nur der Import-Pfad wechselt auf inbox/. Grund: DKV läuft produktiv, der Exchange/NTLM-Pfad ist bekannt fragil (siehe project_calendar_ews_fix); Regressionsrisiko im Fleet-Modul muss null bleiben.
  • D-03: Jedes Modul behält seine eigene, unabhängige Postfach-Konfiguration (eigene Adresse, eigene Credentials). Geteilt ist nur der Code, nicht die Config.

E-Mail-Alert-Parsing (INGEST-05)

  • D-04: Generische Auswertung statt portalspezifischer Parser. Aus jeder Alert-Mail werden die enthaltenen Links (auf die Ausschreibungs-Detailseite) plus Betreff/erste Textzeile als Titel extrahiert. Grund: Es sind keine konkreten Alert-Portale bekannt (User kennt aktuelle Praxis nicht), daher wären portalspezifische Parser Raterei. Feldarmut (kein CPV/Buyer/Frist) wird bewusst und offen dokumentiert — analog zur NetServer-Deep-Link-Beschränkung (D-01 in Phase 13).
  • D-05: Konsequenz für Cross-Source-Dedup: E-Mail-/RSS-Records haben dünne Felder (leeres CPV) und erben damit dieselbe Fingerprint-Asymmetrie wie die Scraper aus Phase 13 (siehe project_resume_2026-07-22, residual_gap_decision = Option C, dormant). Kein neuer Dedup-Mechanismus in Phase 14; das Thema bleibt bis zur Live-Aktivierung einer zweiten Quelle vertagt.

Config-Modell + Admin-UI (CONFIG-02, CONFIG-03)

  • D-06: Postfach-Konfiguration ist pro Mandant (tenantId-scoped), Muster wie DkvModuleConfig (schema.prisma:180: tenantId @unique + verschlüsselte Creds). Neues Prisma-Modell, kein Ausbau des globalen TenderSourcePollConfig-Singletons.
  • D-07: Zugangsdaten werden mit CalendarCryptoService (AES-256-GCM, Format iv:authTag:ciphertext, Key aus env) verschlüsselt — dasselbe Verfahren, das DKV und der SMTP-Versand bereits nutzen. Verschlüsselte Felder werden aus GET-Responses ausgeschlossen (Safe-Select-Muster, T-07-12).
  • D-08: RSS-Feeds sind öffentlich und für alle gleich → globale Plattform-Konfiguration (bestehendes Singleton-Muster), nicht pro Mandant.
  • D-09: Die bestehende Admin-Seite (apps/web/.../tender-radar/settings/) wird erweitert, nicht neu gebaut: ein Abschnitt „E-Mail-Alerts" (pro Mandant, durch Mandanten-Admin konfigurierbar) und einer „RSS-Feeds" (global). Rollen-Trennung über bestehendes @Roles(ADMIN, SUPER_ADMIN).

i18n (UI-06)

  • D-10: „Zweisprachig" = alle Modul-Texte liegen in Deutsch UND Englisch vor. Ein neuer tenderRadar-Namensraum wird in apps/web/src/messages/de.json und en.json angelegt; alle hartcodierten deutschen Strings der tender-radar-Komponenten werden in useTranslations-Keys überführt (ResultsList, FilterPanel, SavedSearchBar, TenderDetail, CoverageBanner, Settings-Seite, SourceConfigForm).
  • D-11: KEIN sichtbarer Sprachumschalter. Die Sprachwahl folgt dem bestehenden next-intl-Mechanismus. Ein app-weiter Language-Switcher ist ausdrücklich NICHT Teil dieser Phase (siehe Deferred Ideas).

Denylist-Transparenz (UI-06)

  • D-12: vergabe24 und aumass werden im UI als „manuell beobachten" mit Direktlink dargestellt (statt stiller Lücke). Vorhandene CoverageBanner.tsx-Komponente ist der natürliche Ort. Denylist-Quelle bleibt der code-level Gate DENYLISTED_PORTALS (source-registry.ts:12).

Claude's Discretion

  • Wahl der RSS-Parsing-Bibliothek und das genaue Feld-Mapping der RSS-Items auf NormalizedTenderFields (generische Bag, analog zu Scraper-Adaptern).
  • Genaue Struktur des neuen inbox/-Modul-Interfaces und der zusätzlichen Message-Methode.
  • Scheduler-Anbindung der neuen Quellen (RSS-Adapter, E-Mail-Adapter) über das bestehende SourceRegistry + pollDueSources-Muster.
  • EN-Übersetzungen erstellt Claude selbst (Fachbegriffe der Vergabe-Domäne konsistent halten).

<canonical_refs>

Canonical References

Downstream agents MUST read these before planning or implementing.

Inbox-Refactor (INGEST-05)

  • apps/api/src/dkv/providers/inbox-provider.interface.ts — bestehendes InboxProvider-Interface (fetchPdfAttachments, testConnection); Basis der Extraktion.
  • apps/api/src/dkv/providers/imap.provider.ts — ImapProvider (IMAP-Verbindung).
  • apps/api/src/dkv/providers/exchange-inbox.provider.ts — ExchangeInboxProvider (EWS/NTLM via httpntlm).
  • apps/api/src/dkv/dkv.types.ts §47/§69/§78 — InboxConfig, InboxAttachment, InboxEmail.
  • apps/api/src/dkv/dkv.service.ts §82-83/§117-118 — aktuelle Injektion + Nutzung; Import-Pfad wechselt hier auf inbox/.
  • apps/api/src/dkv/dkv.module.ts §52-53 — Provider-Registrierung.

Verschlüsselung (CONFIG-03)

  • apps/api/src/calendar/crypto.service.ts §18 — CalendarCryptoService.encrypt/decrypt, AES-256-GCM, Key aus env.
  • apps/api/prisma/schema.prisma §180/§194 — DkvModuleConfig (per-tenant Vorlage) mit encryptedInboxCreds.
  • apps/api/src/settings/settings.service.ts §30 — SMTP-Wiederverwendung desselben Krypto-Musters (Safe-Select).

Quellen-Ingestion + Registry (INGEST-04/05)

  • apps/api/src/tenders/adapters/tender-source-adapter.interface.ts §15 — Adapter-Vertrag (sourceType, portals[], fetchTenders(dayCursor)); Docstring nennt RSS+E-Mail als Zukunfts-Adapter.
  • apps/api/src/tenders/source-registry.ts §12/§35 — SourceRegistry, Denylist-Gate DENYLISTED_PORTALS.
  • apps/api/src/tenders/tenders.module.ts §100-115/§129-138/§162-213 — Provider-Registrierung, onModuleInit-Registrierung, Poll-Config-Seeds (Plug-in-Punkt neuer Adapter).
  • apps/api/src/tenders/tender-ingestion.service.ts §75/§98/§118/§144 — pollDueSources-Fan-out über Poll-Configs.
  • apps/api/src/tenders/tender-scheduler.service.ts §49/§115 — globaler Cron.
  • apps/api/src/tenders/tender.types.ts — RawTenderRecord, NormalizedTenderFields, SourceType-Union (um RSS/E-Mail-Typen erweitern).
  • apps/api/src/tenders/tender-normalizer.service.ts — normalizeBag()-Pfad (Quick 260723-e7i) ist die Vorlage für generische Bag-Normalisierung von RSS/E-Mail-Records.

Poll-Config + Admin-UI (CONFIG-02)

  • apps/api/prisma/schema.prisma §416 — TenderSourcePollConfig (globales Singleton, kein tenantId).
  • apps/api/src/tenders/tenders.controller.ts §147/§383 — bestehende GET/PUT /source-config, @Roles(ADMIN, SUPER_ADMIN).
  • apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx + settings/components/SourceConfigForm.tsx §29 — zu erweiternde Admin-Seite.
  • apps/web/src/lib/tender-radar-api.ts — API-Client.

Multi-Tenancy

  • apps/api/src/prisma/prisma-tenant.extension.ts §9 — forTenant() (RLS via set_config('app.current_tenant', …)).
  • apps/api/src/tenders/tender-mail.service.ts §62/§153 — per-Tenant SMTP-Auflösung (getDecryptedSmtpConfig(tenantId)), Muster für per-Tenant Mailbox-Auflösung.

i18n (UI-06)

  • apps/web/src/messages/de.json, apps/web/src/messages/en.json — Message-Dateien (575 Zeilen, parallel); neuer tenderRadar-Key fehlt bisher.
  • apps/web/src/app/layout.tsx — next-intl-Provider (Sprachquelle heute).
  • apps/web/src/middleware.ts — JWT-only, KEIN Locale-Routing.
  • tender-radar-Komponenten (alle hartcodiert Deutsch): apps/web/src/app/(portal)/modules/tender-radar/components/{ResultsList,FilterPanel,SavedSearchBar,TenderDetail,CoverageBanner}.tsx, .../page.tsx, .../settings/page.tsx, .../settings/components/SourceConfigForm.tsx.

Phase-Fahrplan

  • .planning/ROADMAP.md §453 — Phase-14-Ziel + 5 Success Criteria.
  • .planning/REQUIREMENTS.md — INGEST-04, INGEST-05, CONFIG-02, CONFIG-03, UI-06.

</canonical_refs>

<code_context>

Existing Code Insights

Reusable Assets

  • CalendarCryptoService (AES-256-GCM) — direkt wiederverwendbar für Mailbox-Credentials; von DKV + SMTP bereits genutzt.
  • ImapProvider / ExchangeInboxProvider — Verbindungsmechanik wiederverwendbar; werden nach inbox/ gehoben.
  • SourceRegistry + pollDueSources + Adapter-Interface — RSS- und E-Mail-Adapter plugen sich exakt wie NetServer/cosinex ein (Interface implementieren → Provider + register in tenders.module.ts → Poll-Config-Zeile seeden).
  • normalizeBag() in tender-normalizer.service.ts — Vorlage für die generische Normalisierung dünn gefüllter RSS/E-Mail-Records.
  • DkvModuleConfig (per-tenant, tenantId @unique, verschlüsselte Creds) — Struktur-Vorlage für das neue Mailbox-Config-Modell.
  • CoverageBanner.tsx — vorhandener Ort für die Denylist-Transparenz.
  • next-intl ist app-weit eingerichtet; de.json/en.json existieren mit paralleler Struktur.

Established Patterns

  • Safe-Select: verschlüsselte Credential-Felder werden aus GET-Responses ausgeschlossen (T-07-12).
  • Per-Tenant-Config: tenantId @unique-Modell + Auflösung im Service pro Anfrage (wie SMTP in tender-mail.service.ts).
  • Generische Bag-Normalisierung: Adapter legen dünne Felder in ocdsPayload, normalizeBag() mappt per sourceType.

Integration Points

  • Neuer RSS-Adapter + neuer E-Mail-Adapter → registriert in tenders.module.ts onModuleInit.
  • Neues inbox/-Modul → importiert von DKV (Pfadwechsel) und vom neuen E-Mail-Adapter.
  • Neues Mailbox-Config-Modell → schema.prisma (Migration), verschlüsselt via CalendarCryptoService.
  • Admin-UI → Erweiterung der bestehenden tender-radar-Settings-Seite.
  • tenderRadar-i18n-Namensraum → de.json/en.json + useTranslations in allen tender-radar-Komponenten.

</code_context>

## Specific Ideas
  • Das Ausschreibungs-Postfach ist ausdrücklich ein anderes Postfach als das DKV-Rechnungs-Postfach (User-Klarstellung). Adresse + Zugangsdaten werden in den Modul-Einstellungen hinterlegt.
  • „Zweisprachig" bedeutet: übersetzte Strings genügen; kein Umschalt-Knopf für den Endnutzer.
## Deferred Ideas
  • App-weiter Sprachumschalter — ein sichtbares UI-Element, mit dem ein Benutzer aktiv zwischen DE und EN wechselt (inkl. Persistenz in User-Präferenz/Cookie und ggf. Locale-Routing). Betrifft die gesamte Anwendung, nicht nur tender-radar → eigene Plattform-Phase, nicht Teil von Phase 14.
  • Portalspezifische E-Mail-Parser — präzise Parser pro Alert-Portal (bessere Feldqualität als die generische Extraktion) → sinnvoll erst, wenn konkrete Alert-Portale und echte Mail-Samples bekannt sind.
  • Cross-Source-Fingerprint für dünne Quellen — CPV-loser Zweit-Fingerprint (Option A aus Phase-13-Entscheidung), damit RSS/E-Mail-/Scraper-Records mit DÖE deduplizieren → erst bei Live-Aktivierung einer zweiten Quelle.

Phase: 14-rss-email-alert-ingestion-module-rollout Context gathered: 2026-07-23