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

139 lines
7.2 KiB
Markdown

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