Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.0 KiB
Phase 12: Tender Notifications - Context
Gathered: 2026-07-22 Status: Ready for planning Mode: mvp (vertical slices)
## Phase BoundaryProaktive E-Mail-Benachrichtigung über neue passende Ausschreibungen. Baut auf Phase-11-Suchprofilen (TenderSavedSearch) auf: das System wertet neu eingelesene DÖE-Ausschreibungen gegen die aktiven Suchprofile der Nutzer aus, erzeugt „Treffer" (matches) und benachrichtigt per konfigurierbarem Digest und/oder Sofort-Alert — ohne Doppelversand und ohne Rückstau-Massenversand. Versand über die mandantenspezifische SMTP-Konfiguration (bestehendes DKV-Mail-Muster).
Requirements: NOTIFY-01, NOTIFY-02, NOTIFY-03, NOTIFY-04.
Nicht in dieser Phase: Scraping-Adapter + Cross-Source-Dedup (Phase 13), RSS/E-Mail-Quellen-Ingestion + Admin-Quellen-UI + i18n-Rollout + ausgeschlossene-Portale-UI (Phase 14). Keine In-App-/Push-/Webhook-Benachrichtigungen — nur E-Mail.
## Implementation DecisionsBestätigt durch User-Abfrage 2026-07-22:
Digest (NOTIFY-01)
- D-01: Wählbare Intervalle im Webinterface: Täglich (Default), Wöchentlich, Aus. (Stündlich bewusst NICHT angeboten — zu mail-lastig; kann später ergänzt werden.) „Aus" = kein Digest, Nutzer verlässt sich auf Sofort-Alerts.
- D-02: Digest-Aufbau: eine einzige Mail pro Nutzer, innerhalb nach Suchprofil gegliedert (Abschnitte je Profil). Nicht eine Mail pro Profil — hält die Mail-Flut niedrig.
- D-03: Digest-Intervall ist eine Nutzer-Einstellung (pro Nutzer, mandantenbewusst), nicht pro Profil. Ein Digest deckt alle Profile des Nutzers ab.
Sofort-Alert (NOTIFY-02)
- D-04: Sofort-Alerts sind pro Suchprofil aktivierbar, Default AUS. Nutzer schaltet sie bewusst pro Profil ein. Verhindert ungewollte Mail-Flut.
- D-05: Ein Sofort-Alert = eine Mail kurz nach einem neuen Treffer gegen dieses Profil. Mehrere gleichzeitig neu eingelesene Treffer desselben Profils dürfen zu einer Sammel-Sofort-Mail gebündelt werden (Claude's Discretion: Einzelmail vs. kurze Bündelung pro Poll-Tick) — aber nie ein Alert je Profil-Treffer-Paar, das schon benachrichtigt wurde.
Getroffen vs. Benachrichtigt (NOTIFY-03) — Kern-Invariante
- D-06: Expliziter matched-vs-notified-State. Ein Treffer (Tender × Suchprofil) wird als eigener Datensatz erfasst („getroffen"), separat davon der Benachrichtigungsstatus („benachrichtigt via digest/instant"). Ein Tender+Profil-Paar wird nie zweimal benachrichtigt — weder Digest und Sofort, noch zweimal im selben Kanal.
- D-07: Rückstau-Unterdrückung beim Anlegen/Aktivieren eines Suchprofils: vorhandene historische Treffer (der ~2150 Bestands-Ausschreibungen) werden beim ersten Aktivieren als „bereits benachrichtigt"/unterdrückt markiert — der Nutzer bekommt KEINE Flut Einzelmails für Altbestand. Nur ab-jetzt-neue Treffer lösen Benachrichtigungen aus. (Passt zu Phase-10-D-01 „nur ab jetzt".)
Versand (NOTIFY-04)
- D-08: E-Mail-Versand über die mandantenspezifische SMTP-Konfiguration (
SettingsService/SmtpConfig+MailModule, exakt wieDkvMailService), NICHT über einen globalen System-Mailer.
Claude's Discretion
- Matching-Mechanismus: wie neu eingelesene Tender gegen aktive Profile ausgewertet werden — bevorzugt Wiederverwendung der Phase-11-
tender-query.builder.ts-where-Logik (Profil-filters-JSON → Prisma-where), ausgelöst am Ende des Ingestion-Poll-Ticks (TenderIngestionService.pollDueSources) oder als separater Match-Scheduler. Match-Granularität, Batch-Größe, Performance. - Datenmodell: neue Tabelle(n) für Treffer + Benachrichtigungsstatus (z.B.
TenderMatchmitnotifiedDigestAt/notifiedInstantAtoder getrennte notification-log-Tabelle) — Namen, Unique-Constraints (Tender×Profil), Scoping (where:{userId}wie Phase-11-Triage, kein RLS/forTenant). - Digest-Scheduler (
@nestjs/schedule), Zeitpunkt-Berechnung (täglich/wöchentlich), Sofort-Alert-Auslösung am Poll-Tick. - E-Mail-Templating (HTML/Text) — schlicht, hardcodiertes Deutsch (i18n = Phase 14).
- Web-UI: Digest-Intervall-Einstellung (pro Nutzer) + Sofort-Alert-Toggle pro Profil (erweitert die Phase-11-
SavedSearchBar/Settings).
<canonical_refs>
Canonical References
Downstream agents MUST read these before planning or implementing.
Phasen-Vorgaben
.planning/ROADMAP.md§ Phase 12 — Ziel + 4 Erfolgskriterien. Mode: mvp..planning/REQUIREMENTS.md— NOTIFY-01..04.
Phase-11-Fundament (verbindlich — Matching baut darauf auf)
apps/api/src/tenders/tender-query.builder.ts—buildTenderWhere(Profil-filters→ Prisma-where); wiederverwenden fürs Matching.apps/api/prisma/schema.prisma→TenderSavedSearch(userId+tenantId,filters Json),TenderTriage,Tender(global).apps/api/src/tenders/tender-saved-search.service.ts+tender-triage.service.ts— per-userwhere:{userId}-Scoping-Muster (kein RLS/forTenant) für die neuen notification-Tabellen.apps/api/src/tenders/tender-ingestion.service.ts(pollDueSources) +tender-scheduler.service.ts— Poll-Tick als Auslösepunkt fürs Matching;@nestjs/schedule/SchedulerRegistry-Muster für den Digest-Scheduler.
DKV-Mail-Muster (verbindlich für NOTIFY-04)
apps/api/src/dkv/dkv-mail.service.ts— Vorlage: mandanten-SMTP-Versand.apps/api/src/mail/mail.module.ts+apps/api/src/settings/settings.service.ts(getStartupSmtpConfig) +apps/api/src/settings/dto/smtp-config.dto.ts— SMTP-Konfig-Auflösung + Transport.apps/api/src/calendar/crypto.service.ts— SMTP-Credential-Entschlüsselung (wird von DKV-Mail wiederverwendet).
Milestone-Research
.planning/research/ARCHITECTURE.md— matched-vs-notified-State, backfill-Suppression, global-vs-tenant-Split..planning/research/PITFALLS.md— Backfill-Massenversand, Doppelversand Digest+Instant, Multi-Tenant-Scheduler. </canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
DkvMailService+MailModule+SettingsService.getStartupSmtpConfig— kompletter mandanten-SMTP-Versandpfad; NOTIFY-04 verdrahtet hier, kein neuer Mailer.tender-query.builder.ts— die Filter-where-Logik der Suchprofile; Matching = „welche Tender erfüllen das gespeichertefilters-JSON".tender-ingestion.service.ts/tender-scheduler.service.ts— Poll-once-fan-out-many-Scheduler + Poll-Tick als natürlicher Match-Auslöser;@nestjs/schedulefür den Digest-Job.tender-saved-search.service.ts/tender-triage.service.ts— per-user-Scoping-Muster für neue notification-Tabellen.
Established Patterns
- Prisma-Migrationen als handgeschriebene timestamped Ordner unter
apps/api/prisma/migrations/; lokal viadocker compose exec -T db psqlanwenden (kein Docker-Deploy Testserver). - Native
fetch/Plain-fetch(kein TanStack Query); shadcn/ui; hardcodiertes Deutsch (i18n = Phase 14). - SMTP-Credentials verschlüsselt (CalendarCryptoService).
Integration Points
- Neue Tabelle(n): Treffer + Benachrichtigungsstatus (Tender×Profil), per-user-gescoped.
- Match-Erzeugung hängt am Ingestion-Poll-Tick (neue Tender → gegen aktive Profile prüfen).
- Digest-Scheduler (täglich/wöchentlich) liest un-benachrichtigte Treffer, sendet eine gruppierte Mail, markiert als benachrichtigt.
- Sofort-Alert am Poll-Tick für Profile mit
instantAlert=true. - Web-UI erweitert Phase-11-Suchprofil-/Settings-Oberfläche (Digest-Intervall pro Nutzer, Sofort-Toggle pro Profil). </code_context>
- Kern-Risiko dieser Phase (aus Milestone-Research + ROADMAP): kein Doppelversand, kein Rückstau-Flut. Der explizite matched-vs-notified-State (D-06) und die Backfill-Suppression bei Profil-Aktivierung (D-07) sind die zwei Pflicht-Mechanismen — Erfolgskriterium 3 steht und fällt damit.
- „nur ab jetzt"-Semantik konsistent mit Phase 10 (D-01): historische Treffer beim ersten Profil-Aktivieren unterdrücken, nicht nachversenden.
- Mandanten-SMTP wiederverwenden — Tessera ist Multi-Tenant-Produkt, kein globaler Absender.