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

8.0 KiB
Raw Permalink Blame History

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_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>
## 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.