# Phase 12: Tender Notifications - Context **Gathered:** 2026-07-22 **Status:** Ready for planning **Mode:** mvp (vertical slices) ## Phase Boundary Proaktive 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 Decisions Bestä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 wie `DkvMailService`), 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. `TenderMatch` mit `notifiedDigestAt`/`notifiedInstantAt` oder 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 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-user `where:{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. ## 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 gespeicherte `filters`-JSON". - `tender-ingestion.service.ts` / `tender-scheduler.service.ts` — Poll-once-fan-out-many-Scheduler + Poll-Tick als natürlicher Match-Auslöser; `@nestjs/schedule` fü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 via `docker compose exec -T db psql` anwenden (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). ## Specific Ideas - 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.