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
+43
View File
@@ -1,4 +1,5 @@
import { ModuleRegistryService } from '../module-registry/module-registry.service';
import type { PrismaService } from '../prisma/prisma.service';
/**
* Seeds the tender-radar module into the module registry.
@@ -24,3 +25,45 @@ export async function seedTendersModule(
isSystem: true,
});
}
/** The one genuinely national/global RSS source (RESEARCH.md Open Question 3). */
export const SERVICE_BUND_RSS_URL =
'https://www.service.bund.de/Content/Globals/Functions/RSSFeed/RSSGenerator_Ausschreibungen.xml';
/**
* Seeds the single default-active service.bund.de RSS feed row — the one
* genuinely national/global RSS source (RESEARCH.md Open Question 3,
* Phase 14, Plan 02, D-14). subreport-elvis has no single canonical URL
* (per-municipality instances, RESEARCH.md Pitfall 2) — deliberately ZERO
* subreport-elvis rows are ever seeded; users add the municipality feeds
* relevant to them via the RSS-Feeds UI (Plan 14-02 Task 3).
*
* Extracted out of `TendersModule.onModuleInit` (Phase 17, Plan 02, Task 3)
* — same "logic lives in a plain testable function, the module's lifecycle
* hook only wraps it in try/catch + logging" pattern as `seedTendersModule`
* above — so the idempotency behavior below can be exercised directly by a
* unit test instead of only through a full Nest bootstrap.
*
* Phase 17, Plan 02 (D-02): "upsert on url" stopped working when the
* unique index moved to `(userId, url)` — 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. This function instead FINDS a
* platform-wide row with this url first, and only creates one if none
* exists — an ordinary equality condition has no such requirement. Still
* idempotent across repeated app starts (proven in tenders.seed.spec.ts).
*/
export async function seedServiceBundRssFeed(prisma: PrismaService): Promise<void> {
const existing = await prisma.tenderRssFeedSource.findFirst({
where: { userId: null, url: SERVICE_BUND_RSS_URL },
});
if (existing) return;
await prisma.tenderRssFeedSource.create({
data: {
url: SERVICE_BUND_RSS_URL,
label: 'service-bund',
isActive: true,
},
});
}