docs(17): plan per-user tender sources
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -122,6 +122,7 @@ Progress: [██████████] 100%
|
||||
|
||||
### Roadmap Evolution
|
||||
|
||||
- Phase 17 added (2026-08-12): Eigene Ausschreibungs-Quellen je Nutzer. TenderEmailConfig (heute `tenantId @unique`) und TenderRssFeedSource (heute `url @unique`, plattformweit) wandern auf `userId`; die Rollenpruefung faellt fuer diese beiden Abschnitte weg, das Abrufintervall der oeffentlichen Quelle bleibt Admin-Sache. Ausschreibungsdaten bleiben plattform-global (D-03 aus Phase 10 unangetastet) — geaendert wird nur, wer Quellen einspeist, nicht wer Treffer sieht. Ausloeser: Backlog `2026-08-11-tender-radar-einstellungen-mischen-rollen.md`; die urspruengliche Zustimmung zur gemeinsamen Konfiguration beruhte auf einer missverstaendlichen Erklaerung.
|
||||
- Phase 15 added (2026-08-04): Modul-Berechtigungen — Gruppen & User-Grants. Zweistufiger Modulzugriff (Mandanten-Aktivierung + Grants pro Gruppe/User), Gruppen mit optionaler AD-Bindung, default geschlossen, ADMIN/SUPER_ADMIN umgehen Grants, nur Zugriff an/aus. Startet Milestone v1.2 Plattform-Berechtigungen.
|
||||
|
||||
### Decisions
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user