test(17-02): D-06 ownerTenantId tagging for RSS ingestion + seed idempotency

- RssAdapter.fetchTenders tags every record from a feed with a tenantId
  with the same D-13 ownerTenantId origin marking email-alert records
  carry since Phase 14; platform-wide feeds (no tenantId) stay unmarked.
  The pure parseFeed mapping is untouched — tagging happens in the
  fan-out loop that knows which row a batch came from
- Extracted the service.bund.de seed out of TendersModule.onModuleInit
  into seedServiceBundRssFeed() (tenders.seed.ts, same pattern as the
  existing seedTendersModule), so the find-then-create idempotency added
  in Task 1 is unit-tested directly instead of only via a Nest bootstrap
- New rss-feed-migration-sql.spec.ts: text-only check of the Task 1
  migration file (nullable columns, dropped/created indexes, no
  existing-row mutation, correct ordering)

- Files modified: apps/api/src/tenders/adapters/rss.adapter.ts, apps/api/src/tenders/tenders.module.ts, apps/api/src/tenders/tenders.seed.ts, apps/api/src/tenders/adapters/rss.adapter.spec.ts, apps/api/src/tenders/tenders.seed.spec.ts, apps/api/src/tenders/rss-feed-migration-sql.spec.ts
This commit is contained in:
2026-08-12 11:45:27 +02:00
parent 96161556db
commit 7dee116254
6 changed files with 306 additions and 37 deletions
+8 -32
View File
@@ -10,7 +10,7 @@ import { EmailAlertAdapter } from './adapters/email-alert.adapter';
import { NetServerAdapter } from './adapters/netserver.adapter';
import { RssAdapter } from './adapters/rss.adapter';
import { SourceRegistry } from './source-registry';
import { seedTendersModule } from './tenders.seed';
import { seedServiceBundRssFeed, seedTendersModule } from './tenders.seed';
import { TenderDedupService } from './tender-dedup.service';
import { TenderDigestScheduler } from './tender-digest.scheduler';
import { TenderEmailConfigService } from './tender-email-config.service';
@@ -278,37 +278,13 @@ export class TendersModule implements OnModuleInit {
}
try {
// Phase 14, Plan 02 (D-14, RESEARCH.md Open Question 3): seed a
// 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/users add
// the municipality feeds relevant to them via the RSS-Feeds UI
// (Plan 14-02 Task 3).
//
// 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',
},
});
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,
},
});
}
// Phase 17, Plan 02 (Task 3): the actual find-then-create logic (and
// why "upsert on url" stopped working, D-02) lives in
// seedServiceBundRssFeed (tenders.seed.ts) now — extracted the same
// way seedTendersModule already is, so it is a plain testable
// function and not only reachable via a full Nest bootstrap
// (idempotency proven in tenders.seed.spec.ts).
await seedServiceBundRssFeed(this.prisma);
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);