Files
2026-07-22 08:33:10 +02:00

100 lines
8.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 12: Tender Notifications - Context
**Gathered:** 2026-07-22
**Status:** Ready for planning
**Mode:** mvp (vertical slices)
<domain>
## 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.
</domain>
<decisions>
## 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).
</decisions>
<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-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.
</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 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).
</code_context>
<specifics>
## 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.
</specifics>