From 9ac1142fcdfa38f7c7a93e9b01bd752852100a62 Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 22 Jul 2026 08:33:10 +0200 Subject: [PATCH] =?UTF-8?q?docs(12):=20phase=20context=20=E2=80=94=20diges?= =?UTF-8?q?t/instant=20alerts,=20matched-vs-notified,=20tenant=20SMTP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../12-tender-notifications/12-CONTEXT.md | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 .planning/phases/12-tender-notifications/12-CONTEXT.md diff --git a/.planning/phases/12-tender-notifications/12-CONTEXT.md b/.planning/phases/12-tender-notifications/12-CONTEXT.md new file mode 100644 index 0000000..199a37e --- /dev/null +++ b/.planning/phases/12-tender-notifications/12-CONTEXT.md @@ -0,0 +1,99 @@ +# 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. +