Files
tessera-ctl/.planning/phases/17-eigene-ausschreibungs-quellen-je-nutzer/17-CONTEXT.md
T
schalli 42a2c7703f
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 47s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s
docs(17): plan per-user tender sources
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 08:28:44 +02:00

7.2 KiB

Phase 17 — Kontext: Eigene Ausschreibungs-Quellen je Nutzer

Quelle dieses Dokuments: Gespraech mit dem User am 2026-08-12. Kein /gsd-discuss-phase-Lauf — die Entscheidungen fielen im Gespraech beim Durchsprechen des Backlog-Punkts 2026-08-11-tender-radar-einstellungen-mischen-rollen.md.

Warum diese Phase existiert

Die Einstellungsseite /modules/tender-radar/settings traegt vier Abschnitte, die drei verschiedenen Zustaendigkeiten gehoeren. Der Backlog-Punkt sah darin urspruenglich nur ein Anzeigeproblem ("Admin-Bloecke hinter eine Rollenpruefung legen"). Beim Durchsprechen kam heraus, dass zwei der drei Admin-Abschnitte inhaltlich falsch zugeschnitten sind:

"Wenn Person A ihre Ausschreibungen auf kontoa@mail.de und Person B von kontob@mail.de holt, macht eine gemeinsame Einstellung keinen Sinn. Ebenso die RSS-Feeds. Wieso kann eine Person nicht mehrere Portale abfragen als eine andere?"

Der User hat ausdruecklich festgehalten, dass seine urspruengliche Zustimmung zur gemeinsamen Konfiguration auf einer missverstaendlichen Erklaerung meinerseits beruhte. Das ist keine Meinungsaenderung, sondern eine Korrektur.

Heutiger Zustand, konkret: TenderEmailConfig.tenantId ist @unique — es gibt genau ein Alert-Postfach pro Mandant. Ein zweiter Nutzer mit eigenem Portal-Konto kann seine Alerts gar nicht anbinden. TenderRssFeedSource.url ist @unique ohne Besitzer — die Feed-Liste gilt fuer alle, und wer einen Feed loescht, nimmt ihn allen weg.

Entscheidungen

D-01 — Alert-Postfach gehoert dem Nutzer

TenderEmailConfig wechselt von tenantId @unique auf userId @unique. Jeder Nutzer bindet sein eigenes Postfach an. tenantId bleibt als denormalisiertes Feld erhalten (SMTP-Aufloesung, gleiche Rolle wie in TenderMatch).

D-02 — RSS-Feeds bekommen einen Besitzer, plattformweite Feeds bleiben moeglich

TenderRssFeedSource bekommt ein nullable userId:

  • userId = null → plattformweiter Feed, von der Administration gepflegt, gilt fuer alle. Der in Phase 14 gesetzte service.bund.de-Feed bleibt damit unveraendert fuer jeden aktiv.
  • userId gesetzt → persoenlicher Feed, nur dieser Nutzer bekommt ihn.

url @unique faellt weg und wird @@unique([userId, url]), sonst koennten nicht zwei Nutzer denselben Feed verfolgen.

Begruendung: Die Alternative — alle Feeds persoenlich, niemand startet mit etwas — haette den seit Phase 14 aktiven Standard-Feed fuer alle bestehenden Nutzer stillgelegt. Diese Entscheidung ist im Gespraech nicht ausdruecklich abgefragt worden; sie ist der am wenigsten zerstoererische Weg und sollte beim Planen gegengelesen werden.

D-03 — Abrufintervall bleibt Administration

TenderSourcePollConfig bleibt unveraendert plattformweit. Es gibt genau eine oeffentliche Quelle (oeffentlichevergabe.de), die fuer alle dieselben Daten liefert. Pro Nutzer abzurufen hiesse, dieselben Daten mehrfach zu holen. Der User hat dem im Gespraech ausdruecklich zugestimmt.

D-04 — Digest-Intervall ist bereits nutzereigen, muss nur sichtbar werden

TenderNotificationPref haengt schon an userId (NOTIFY-01/D-03 aus Phase 12). Die Einstellung steht heute nur an der falschen Stelle: als letzter Abschnitt unter drei Admin-Bloecken. Kein Datenmodell-Aenderung noetig, nur Platzierung.

D-05 — Ausschreibungsdaten bleiben plattform-global (HARTE GRENZE)

Tender bleibt ohne tenantId, ohne RLS, ohne forTenant() — Entscheidung D-03 aus Phase 10 bleibt unangetastet. Geaendert wird ausschliesslich, wer Quellen einspeist, nicht wer Treffer sieht.

Der User wurde darueber aufgeklaert und hat zugestimmt:

"Reicht dir 'jeder speist seine eigenen Quellen ein, gesehen wird alles gemeinsam'?" → "Ja, plane"

Praktische Folge, die in der UI ehrlich benannt werden muss: Was ueber das Postfach von Nutzer A hereinkommt, steht danach auch in der Trefferliste von Nutzer B. Eine Sichtbarkeitstrennung waere ein deutlich groesserer Umbau (Tender mandanten-/nutzerscoped, RLS, Dedup ueber Sichtbarkeitsgrenzen) und ist hier bewusst ausgeklammert.

Offene Punkte fuer die Planung

  1. Migration der bestehenden Zeilen. Das eine vorhandene TenderEmailConfig-Row (falls auf dem Testserver konfiguriert) braucht einen Besitzer. Vorschlag: dem aeltesten ADMIN des Mandanten zuordnen, nicht loeschen. Zu pruefen, ob ueberhaupt eine Zeile existiert.
  2. Bestehende RSS-Feeds werden zu userId = null (plattformweit) — das ist der Ist-Zustand und aendert fuer niemanden etwas.
  3. email-alert.adapter.ts:143 liest heute bewusst mandantenuebergreifend (findMany({ where: { isActive: true }})) und darf nicht in forTenant() gewickelt werden — der Kommentar dort ist ausdruecklich. Der Wechsel auf userId aendert daran nichts: der Scheduler loest weiterhin alle aktiven Postfaecher in einem Durchlauf auf. Die catch-pro-Postfach-Disziplin muss erhalten bleiben.
  4. Wohin mit den nutzereigenen Abschnitten? Es gibt bereits /settings/general/account. Drei Varianten standen im Backlog-Punkt; die Frage betrifft auch DKV-Fleet (gleiche Bauform) und jedes kuenftige Modul. Beim Planen zu entscheiden — eine eigene nutzerseitige Modulseite scheint am tragfaehigsten, weil modulfremde Einstellungen sich sonst in einer allgemeinen Seite sammeln.
  5. Rollenpruefung der verbleibenden Admin-Abschnitte. Das urspruengliche Anliegen des Backlog-Punkts: die Seite selbst prueft keine Rolle, ein normaler Nutzer sieht die Bedienelemente und laeuft beim Speichern in eine Fehlermeldung. Nach dem Umbau bleibt nur noch das Abrufintervall Admin-Sache — das gehoert hinter eine Rollenpruefung.

Betroffene Stellen (Bestandsaufnahme 2026-08-12)

Datei Rolle
apps/api/prisma/schema.prisma:278 TenderEmailConfig (tenantId @unique)
apps/api/prisma/schema.prisma:550 TenderRssFeedSource (url @unique)
apps/api/prisma/schema.prisma:512 TenderNotificationPref (bereits userId @unique)
apps/api/prisma/schema.prisma:524 TenderSourcePollConfig (bleibt)
apps/api/src/tenders/tender-email-config.service.ts 4 Zugriffe auf tenantId
apps/api/src/tenders/tender-rss-feed.service.ts 3 Zugriffe, kein Besitzer-Scoping
apps/api/src/tenders/adapters/email-alert.adapter.ts:143 Fan-out ueber alle aktiven Postfaecher
apps/api/src/tenders/adapters/rss.adapter.ts:79 Fan-out ueber alle aktiven Feeds
apps/api/src/tenders/tenders.controller.ts:241-301 5 Endpunkte, heute @Roles(ADMIN, SUPER_ADMIN)
apps/api/src/tenders/tenders.module.ts:288 Seed des service.bund.de-Feeds
apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx die vier Abschnitte

Guenstig: Beide Adapter holen heute schon alle aktiven Konfigurationen in einem Durchlauf. Mehrere Postfaecher und Feeds sind keine neue Mechanik, nur mehr Zeilen in der Tabelle. Der Scheduler muss dafuer nicht angefasst werden.

Route-Order-Falle (Bestandswissen)

tenders.controller.ts traegt mehrere Kommentare dazu: statische Routen (rss-feeds, email-config, source-config) MUESSEN vor @Get(':id') deklariert werden, sonst schattet die Parameter-Route sie mit 404 ab. Unit-Tests fangen das nicht. Beim Umbau der Endpunkte erhalten bleiben.