feat(17-02): RSS feeds get an owner — platform-wide vs personal (D-02)

- TenderRssFeedSource.userId/tenantId (nullable): null = platform-wide
  (admin-managed, includes the existing service.bund.de default),
  set = personal feed owned by exactly one user
- Migration replaces url @unique with @@unique([userId, url]) — two
  users can now follow the same address independently; existing rows
  keep an empty owner (platform-wide, unchanged behavior)
- Service: listForUser/createForUser/createPlatform replace list/create
- Controller: GET/POST /rss-feeds move from @Roles(ADMIN,SUPER_ADMIN) to
  @UseModule('tender-radar'); POST with scope:'platform' still requires
  ADMIN/SUPER_ADMIN, checked inline (T-17-08)
- tenders.module.ts seed switched from upsert-on-url to find-then-create
  (Rule 3, pulled forward from Task 3): the new compound unique index
  requires a non-null userId in Prisma's generated type, so a
  platform-wide row can no longer be addressed via upsert

- Files modified: apps/api/prisma/schema.prisma, apps/api/prisma/migrations/20260812110000_tender_rss_feed_owner/migration.sql, 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/tender-rss-feed.service.spec.ts, apps/api/src/tenders/tenders.controller.spec.ts
This commit is contained in:
2026-08-12 11:37:56 +02:00
parent 71dcb302a3
commit adb72f611f
8 changed files with 346 additions and 91 deletions
+22 -9
View File
@@ -282,20 +282,33 @@ export class TendersModule implements OnModuleInit {
// single default-active service.bund.de feed row — the one genuinely
// national/global RSS source. subreport-elvis has no single
// canonical URL (per-municipality instances, RESEARCH.md Pitfall 2)
// — deliberately ZERO subreport-elvis rows seeded; admins add the
// municipality feeds relevant to them via the RSS-Feeds admin UI
// — deliberately ZERO subreport-elvis rows seeded; admins/users add
// the municipality feeds relevant to them via the RSS-Feeds UI
// (Plan 14-02 Task 3).
await this.prisma.tenderRssFeedSource.upsert({
//
// Phase 17, Plan 02 (D-02): "upsert on url" no longer works — the
// unique index moved to (userId, url), and Prisma's generated
// compound-unique input type REQUIRES userId as a plain `string`
// (not `string | null | undefined`), so a platform-wide row
// (userId = null) can never be addressed through it. Switched to
// "find a platform-wide row with this url first, only create if
// none exists" — an ordinary equality condition has no such
// requirement. Still idempotent across repeated app starts.
const existingServiceBundFeed = await this.prisma.tenderRssFeedSource.findFirst({
where: {
userId: null,
url: 'https://www.service.bund.de/Content/Globals/Functions/RSSFeed/RSSGenerator_Ausschreibungen.xml',
},
update: {},
create: {
url: 'https://www.service.bund.de/Content/Globals/Functions/RSSFeed/RSSGenerator_Ausschreibungen.xml',
label: 'service-bund',
isActive: true,
},
});
if (!existingServiceBundFeed) {
await this.prisma.tenderRssFeedSource.create({
data: {
url: 'https://www.service.bund.de/Content/Globals/Functions/RSSFeed/RSSGenerator_Ausschreibungen.xml',
label: 'service-bund',
isActive: true,
},
});
}
this.logger.log('service.bund.de default RSS feed seeded (active)');
} catch (error) {
this.logger.error('Failed to seed service.bund.de RSS feed', error);