Files
tessera-ctl/.planning/phases/17-eigene-ausschreibungs-quellen-je-nutzer/17-02-SUMMARY.md
T

17 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions requirements-completed coverage duration completed status
17-eigene-ausschreibungs-quellen-je-nutzer 02 api
prisma
postgresql
nestjs
multi-tenancy
migration
rss
phase provides
17-01 Handgeschriebene Migrationstechnik (migrate diff + Handdatei + migrate deploy), tenantId-Denormalisierungsmuster, Nutzerfreigabe fuer beide Datenbank-Umbauten der Phase
TenderRssFeedSource.userId/tenantId (nullable) — plattformweite vs. persoenliche RSS-Feeds (D-02)
Handgeschriebene Migration (@@unique([userId,url]) ersetzt url @unique), Bestandszeilen unangetastet
TenderRssFeedSourceService: listForUser/createForUser/createPlatform/remove(id,{userId,isAdmin})
GET/POST/DELETE /modules/tender-radar/rss-feeds @UseModule('tender-radar') statt @Roles(ADMIN,SUPER_ADMIN); POST scope:'platform' bleibt admin-only (Inline-Check)
RssAdapter taggt Datensaetze aus persoenlichen Feeds mit ownerTenantId (D-06, D-13-Wiederverwendung)
seedServiceBundRssFeed() (tenders.seed.ts) — find-then-create statt upsert-on-url, testbar ohne Nest-Bootstrap
17-03-ui-aufteilung
tokens tasks commits
19468 3 3
added patterns
Handgeschriebene Prisma-Migration ohne Bestandsdaten-Umzug (nur Spalten+Index-Wechsel) — leerer Besitzer entspricht bereits dem Ist-Zustand, kein Backfill noetig, anders als 17-01
Zusammengesetzte Eindeutigkeitsregel mit einem Feld ohne Pflichtwert: Prismas generierter Compound-Unique-Typ verlangt das Feld trotzdem als Pflicht-String — Zugriff auf die NULL-Seite nur ueber findFirst/deleteMany mit gewoehnlicher Bedingung, nie ueber den Compound-Typ (betraf sowohl die Startbestueckung als auch remove())
Delete-Protection als einzelne bedingte deleteMany-Anweisung (Besitz-Vergleich in der DB-Bedingung, kein TOCTOU-Fenster) statt 'erst lesen, dann loeschen'
Auswertender Prisma-Doppelgaenger in Tests (matchesWhere mit OR/AND/Gleichheit) statt eines Doppelgaengers, der Bedingungen ignoriert und Erfolg vortaeuscht
created modified
apps/api/prisma/migrations/20260812110000_tender_rss_feed_owner/migration.sql
apps/api/src/tenders/rss-feed-migration-sql.spec.ts
apps/api/prisma/schema.prisma
apps/api/src/tenders/tender-rss-feed.service.ts
apps/api/src/tenders/dto/tender-rss-feed.dto.ts
apps/api/src/tenders/tenders.controller.ts
apps/api/src/tenders/tenders.module.ts
apps/api/src/tenders/tenders.seed.ts
apps/api/src/tenders/adapters/rss.adapter.ts
Checkpoint-Freigabe aus 17-01 deckte beide Datenbank-Umbauten der Phase ausdruecklich ab — kein zweiter Halt fuer diese Migration (im Ausfuehrungsauftrag vorgegeben, bestaetigt durch gemessene 1 Bestandszeile lokal, unveraendert plattformweit nach der Migration)
[Rule 3] Startbestueckung (tenders.module.ts) von 'upsert auf url' auf 'erst suchen, dann anlegen' umgestellt bereits in Task 1 statt erst in Task 3 — Task 1s eigene <verify> verlangt einen fehlerfreien Type-Check ueber das gesamte Projekt, und der Compile-Fehler (Prismas generierter Compound-Unique-Typ verlangt userId als Pflicht-String) war sofort da, sobald das Schema in Task 1 geaendert war. Gleiche Vorgehensweise wie in 17-01 (dortige Deviation 1/2).
seedServiceBundRssFeed() aus TendersModule.onModuleInit() in tenders.seed.ts extrahiert (gleiches Muster wie das bereits vorhandene seedTendersModule) — noetig, damit die in Task 3 verlangte Bestueckungs-Idempotenz-Pruefung eine echte, produktiv laufende Funktion testet statt eine Kopie der Logik
DELETE /rss-feeds/:feedId von @Roles(ADMIN,SUPER_ADMIN) auf @UseModule('tender-radar') umgestellt — die Besitzpruefung im Dienst ersetzt die Rollenpruefung vollstaendig (T-17-07)
POST /rss-feeds mit scope:'platform' bleibt admin-only, aber als Inline-Pruefung im Controller (role aus demselben Ort wie RolesGuard) statt als Decorator, weil dieselbe Route jetzt auch den persoenlichen Anlegeweg fuer jeden Modulnutzer bedient (T-17-08)
SRC-02
SRC-03
id description requirement verification human_judgment
D1 TenderRssFeedSource.userId/tenantId ergaenzt (Migration ohne Bestandsdaten-Umzug); @@unique([userId,url]) ersetzt url @unique; listForUser/createForUser/createPlatform ersetzen list/create; GET/POST /rss-feeds sind @UseModule-gated, scope:'platform' bleibt admin-only SRC-02
kind ref status
unit apps/api/src/tenders/tender-rss-feed.service.spec.ts (26 Tests: SSRF/Denylist auf createPlatform, Ownership-Scoping in listForUser, createForUser-Owner-Stempel, gleiche URL fuer zwei Nutzer) pass
kind ref status
unit apps/api/src/tenders/tenders.controller.spec.ts (RSS-Feeds-Block: GET/POST-Delegation, isPlatformWide-Mapping, T-17-08 USER/ADMIN-Faelle) pass
kind ref status
integration prisma migrate deploy gegen lokale Dev-DB (172.19.0.2) + prisma migrate status + Index-Abfrage (TenderRssFeedSource_userId_url_key vorhanden, TenderRssFeedSource_url_key entfernt) pass
false
id description requirement verification human_judgment
D2 Loeschschutz: nur Besitzer oder (Administrator UND plattformweiter Feed) kann loeschen, sonst NotFoundException statt ForbiddenException; Obergrenze 20 persoenliche Feeds je Nutzer; Sperrliste/SSRF-Schutz greift auch auf dem persoenlichen Anlegeweg SRC-02
kind ref status
unit apps/api/src/tenders/tender-rss-feed.service.spec.ts (remove()-Block mit auswertendem Prisma-Doppelgaenger, 6 Faelle; Obergrenze-Block, 2 Faelle; SSRF-auf-createForUser-Block, 2 Faelle) pass
kind ref status
unit apps/api/src/tenders/tenders.controller.spec.ts (DELETE-Faelle mit isAdmin true/false) pass
false
id description requirement verification human_judgment
D3 Ausschreibungen aus einem persoenlichen RSS-Feed tragen die ownerTenantId-Herkunftsmarkierung des Feed-Besitzers (D-06); plattformweite Feeds bleiben unmarkiert; der Sammel-Fetch faengt weiterhin einen ausfallenden Feed ab, ohne die uebrigen zu blockieren, auch ueber Besitzer-Grenzen hinweg SRC-02
kind ref status
unit apps/api/src/tenders/adapters/rss.adapter.spec.ts (D-06-Block: getaggte vs. ungetaggte Datensaetze, zwei Besitzer in einem Durchlauf mit Catch-per-Feed) pass
false
id description requirement verification human_judgment
D4 Der seit Phase 14 gesetzte service.bund.de-Feed bleibt nach der Migration unveraendert aktiv und plattformweit (leerer Besitzer); die Startbestueckung ist bei wiederholten Anwendungsstarts folgenlos (kein Duplikat) SRC-03
kind ref status
unit apps/api/src/tenders/tenders.seed.spec.ts (seedServiceBundRssFeed-Block: Einzelanlage, zweifacher Lauf bleibt bei einer Zeile, vorhandener persoenlicher Feed derselben Adresse bleibt unangetastet) pass
kind ref status
integration SELECT auf TenderRssFeedSource nach angewendeter Migration: genau 1 Zeile, service.bund.de, isActive=true, userId/tenantId leer pass
false
58min 2026-08-12 complete

Phase 17 Plan 02: RSS-Feeds bekommen einen Besitzer Summary

TenderRssFeedSource per Handmigration um nullable userId/tenantId erweitert (D-02) — plattformweite Feeds bleiben admin-gepflegt und fuer alle sichtbar, jeder Modulnutzer kann jetzt zusaetzlich eigene RSS-Feeds anlegen, mit atomarem Loeschschutz, 20er-Obergrenze und ownerTenantId-Herkunftsmarkierung fuer den Abruf.

Performance

  • Duration: 58 min
  • Started: 2026-08-12T11:28:00Z
  • Completed: 2026-08-12T12:26:00Z
  • Tasks: 3 (Tracer-Task durchgehende Bahn, Loeschschutz+Obergrenze, Abruf-Tagging+Startbestueckungs-Test)
  • Files modified: 14 (2 neu, 12 geaendert)

Accomplishments

  • TenderRssFeedSource.userId/tenantId (beide nullable) ersetzen die reine URL-Eindeutigkeit — leerer Besitzer heisst plattformweit (D-02), gesetzt heisst persoenlicher Feed genau eines Nutzers
  • Handgeschriebene Migration ohne Bestandsdaten-Umzug (anders als 17-01): der eine vorhandene service.bund.de-Feed braucht keine Zuordnung, ein leerer Besitzer entspricht bereits dem heutigen Zustand
  • TenderRssFeedSourceService: listForUser/createForUser/createPlatform ersetzen die alte Admin-only-CRUD; remove(id, {userId, isAdmin}) ist eine einzige bedingte deleteMany-Anweisung ohne Zeitfenster zwischen Pruefung und Loeschung
  • GET/POST/DELETE /rss-feeds sind jetzt @UseModule('tender-radar')-gated statt @Roles(ADMIN,SUPER_ADMIN) — jeder Modulnutzer kann eigene Feeds anlegen/sehen/loeschen; POST mit scope: 'platform' bleibt Administratoren vorbehalten (Inline-Pruefung im Controller)
  • Obergrenze von 20 persoenlichen Feeds je Nutzer (T-17-10) — plattformweite Feeds zaehlen nicht mit
  • RssAdapter taggt Datensaetze aus einem Feed mit gesetztem tenantId mit derselben ownerTenantId-Herkunftsmarkierung, die Postfach-Datensaetze seit Phase 14 tragen (D-06); plattformweite Feeds bleiben unmarkiert
  • Startbestueckung des service.bund.de-Feeds als eigene, testbare Funktion seedServiceBundRssFeed() extrahiert (find-then-create statt upsert-on-url) — nachweislich idempotent bei wiederholten Anwendungsstarts

Task Commits

Jede Aufgabe wurde einzeln committet:

  1. Task 1: Ein persoenlicher Feed — durchgehend von der Datenbank bis zum Endpunkt — adb72f6 (feat)
  2. Task 2: Niemand fasst fremde Feeds an — Loeschschutz und Mengenbegrenzung — 9616155 (feat)
  3. Task 3: Abruf und Startbestueckung ziehen nach — 7dee116 (test)

Plan metadata: wird mit diesem SUMMARY committet (docs)

Files Created/Modified

  • apps/api/prisma/schema.prisma — TenderRssFeedSource.userId/tenantId ergaenzt, url @unique durch @@unique([userId, url]) ersetzt, @@index([userId]) ergaenzt
  • apps/api/prisma/migrations/20260812110000_tender_rss_feed_owner/migration.sql — Handmigration: beide Spalten ohne Pflichtwert, alter eindeutiger Index auf der Adresse entfernt, neuer eindeutiger Index ueber Besitzer+Adresse, gewoehnlicher Index auf dem Besitzer; keine Bestandsdaten-Anweisung
  • apps/api/src/tenders/rss-feed-migration-sql.spec.ts — neuer Text-Spec (kein DB-Zugriff), prueft Spalten/Indizes/Reihenfolge der Handmigration
  • apps/api/src/tenders/dto/tender-rss-feed.dto.ts — scope?: 'personal' | 'platform' ergaenzt (Wunsch, keine Berechtigung)
  • apps/api/src/tenders/tender-rss-feed.service.ts — listForUser/createForUser/createPlatform (Task 1), remove(id,{userId,isAdmin}) als bedingte deleteMany, 20er-Obergrenze in createForUser (Task 2)
  • apps/api/src/tenders/tender-rss-feed.service.spec.ts — auf neue Methoden umgestellt, auswertender Prisma-Doppelgaenger (OR/AND/Gleichheit) statt ignorierender Doppelgaenger, 26 Tests gesamt
  • apps/api/src/tenders/tenders.controller.ts — RSS-Feeds-Routen von @Roles auf @UseModule, extractTriageContext um role erweitert, Inline-Rollenpruefung fuer scope:'platform', removeRssFeed reicht {userId,isAdmin} durch
  • apps/api/src/tenders/tenders.controller.spec.ts — Fake-Service-Signaturen angepasst, makeFakeRequest um role erweitert, neue Faelle fuer isPlatformWide-Mapping, DELETE-isAdmin, T-17-08 USER/ADMIN
  • apps/api/src/tenders/tenders.module.ts — Startbestueckung ruft seedServiceBundRssFeed(this.prisma) statt der weggefallenen upsert-Form auf
  • apps/api/src/tenders/tenders.seed.ts — seedServiceBundRssFeed() neu (find-then-create, testbare Funktion)
  • apps/api/src/tenders/tenders.seed.spec.ts — 3 neue Faelle fuer seedServiceBundRssFeed (Einzelanlage, Idempotenz bei zweifachem Lauf, unangetasteter persoenlicher Feed derselben Adresse)
  • apps/api/src/tenders/adapters/rss.adapter.ts — Fan-out taggt Datensaetze mit feed.tenantId als ownerTenantId, wenn gesetzt (D-06); parseFeed unveraendert
  • apps/api/src/tenders/adapters/rss.adapter.spec.ts — 3 neue Faelle fuer D-06-Tagging (getaggt/ungetaggt/zwei-Besitzer-ein-Durchlauf)

Decisions Made

  • Checkpoint-Freigabe aus 17-01 deckte diese Migration bereits ab — kein zweiter Halt, gemessene 1 Bestandszeile blieb nach der Migration unveraendert plattformweit
  • [Rule 3] Startbestueckung bereits in Task 1 auf find-then-create umgestellt statt erst in Task 3 — Task 1s eigener Type-Check-Verify brach sonst sofort
  • seedServiceBundRssFeed() aus dem Modul-Lifecycle-Hook extrahiert, damit die in Task 3 verlangte Idempotenz-Pruefung echten Produktivcode testet
  • DELETE /rss-feeds/:feedId von rollenbasiert auf modulbasiert umgestellt — die Besitzpruefung im Dienst ersetzt die Rollenpruefung vollstaendig
  • POST /rss-feeds mit scope:'platform' bleibt admin-only als Inline-Pruefung, nicht als Decorator, weil dieselbe Route jetzt auch den persoenlichen Weg bedient

Deviations from Plan

Auto-fixed Issues

1. [Rule 3 - Blocking] Startbestueckung (tenders.module.ts) vorgezogen aus Task 3 nach Task 1

  • Found during: Task 1 (Type-Check nach der Schema-Aenderung)
  • Issue: Der Plan warnte in Task 1s eigenem Text ausdruecklich vor diesem Bruch ("betrifft die Startbestueckung, Task 3"), ordnete die Behebung aber Task 3 zu. Task 1s eigenes <verify> verlangt jedoch einen fehlerfreien projektweiten Type-Check — der upsert-Aufruf mit where: { url } kompiliert nicht mehr, sobald @@unique([userId, url]) das Schema ersetzt, weil Prismas generierter Compound-Unique-Typ userId als Pflicht-String verlangt (kann daher keinen plattformweiten Feed mit leerem Besitzer ansprechen)
  • Fix: Startbestueckung auf "erst suchen (Besitzer leer + Adresse), dann anlegen" umgestellt — exakt die in Task 3 beschriebene Loesung, nur zeitlich vorgezogen
  • Files modified: apps/api/src/tenders/tenders.module.ts
  • Verification: pnpm --filter @tessera/api type-check fehlerfrei ab Task 1; DB-Zaehlung bestaetigt den service.bund.de-Feed nach der Migration weiterhin genau einmal vorhanden
  • Committed in: adb72f6 (Task 1 commit)

2. [Rule 3 - Blocking] Test-Dateien an neue Service-/Controller-Signaturen angepasst

  • Found during: Task 1 und Task 2 (Type-Check nach den jeweiligen API-Aenderungen)
  • Issue: tender-rss-feed.service.spec.ts und tenders.controller.spec.ts riefen die alten Methoden list()/create()/remove(id) auf bzw. den Controller ohne req-Parameter — nicht anpassbar ohne Kompilierfehler
  • Fix: Fake-Service-Signaturen und alle betroffenen Testfaelle auf listForUser/createForUser/createPlatform/remove(id,{userId,isAdmin}) umgestellt; makeFakeRequest um role erweitert
  • Files modified: apps/api/src/tenders/tender-rss-feed.service.spec.ts, apps/api/src/tenders/tenders.controller.spec.ts
  • Verification: pnpm --filter @tessera/api exec vitest run src/tenders — 362/362 gruen nach Task 3
  • Committed in: adb72f6, 9616155 (jeweils im zugehoerigen Task-Commit)

Total deviations: 2 auto-fixed (beide Rule 3 — Kompilierfaehigkeit, dieselbe Kategorie wie in 17-01) Impact on plan: Beide Anpassungen waren fuer die Korrektheit der eigentlichen Plan-Aenderung zwingend notwendig. Kein Scope Creep — Deviation 1 ist exakt die von Task 3 selbst beschriebene Loesung, nur zeitlich vorgezogen; Deviation 2 haelt die Test-Suite durchgaengig kompilierbar.

Issues Encountered

  • Lokaler Postgres-Container (tessera-ctl-db-1) lief bereits (aus 17-01) — direkt ueber die Container-IP (172.19.0.2, tessera:tessera_dev) angebunden, keine weitere Vorbereitung noetig
  • Keine weiteren Bloecker

User Setup Required

None — keine externe Dienstkonfiguration erforderlich. Die Migration ist bisher nur lokal angewendet; das Ausrollen auf dem Testserver (192.168.13.12) bleibt bewusst Nutzeraktion beim naechsten Deploy (Projektregel: kein docker compose pull/up/rebuild durch Claude auf dem Testserver).

Next Phase Readiness

  • Backend-Teil von D-02 vollstaendig: Migration lokal angewendet und verifiziert (362/362 src/tenders-Tests, beide Typpruefungen — API und Web — fehlerfrei, Web-Tests 192/192 gruen)
  • Frontend (RssFeedListForm.tsx, aktuell noch hinter der alten Admin-Annahme gebaut) ist bewusst NICHT Teil dieses Plans — Plan 17-03 ("ui-aufteilung") ordnet die Einstellungsseite neu und baut die "Meine Quellen"-Oberflaeche fuer RSS-Feeds analog zu 17-01s Postfach-Seite
  • Rollout auf dem Testserver bleibt Nutzeraktion; beide Migrationen der Phase (17-01 und 17-02) liegen bereit, sind dort aber noch nicht angewendet
  • Kein offener WINDOWS.md-Eintrag aus diesem Plan — alle Pruefungen liefen automatisiert und wurden ausgefuehrt

Self-Check: PASSED

Alle im SUMMARY genannten Dateien existieren auf der Festplatte; alle drei Task-Commit-Hashes (adb72f6, 9616155, 7dee116) sind im Git-Log auffindbar.


Phase: 17-eigene-ausschreibungs-quellen-je-nutzer Completed: 2026-08-12