7dee116254
- 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
70 lines
2.9 KiB
TypeScript
70 lines
2.9 KiB
TypeScript
import { ModuleRegistryService } from '../module-registry/module-registry.service';
|
|
import type { PrismaService } from '../prisma/prisma.service';
|
|
|
|
/**
|
|
* Seeds the tender-radar module into the module registry.
|
|
*
|
|
* Called during TendersModule initialization to ensure the
|
|
* "tender-radar" module record exists in the database (CONFIG-01).
|
|
*
|
|
* isSystem: true registers the module in the registry but does NOT
|
|
* auto-activate it per tenant. Admin must activate via Marketplace UI.
|
|
*/
|
|
export async function seedTendersModule(
|
|
moduleRegistryService: ModuleRegistryService,
|
|
): Promise<void> {
|
|
await moduleRegistryService.seedModule({
|
|
slug: 'tender-radar',
|
|
name: 'Ausschreibungs-Radar',
|
|
version: '1.0.0',
|
|
category: 'procurement',
|
|
description: {
|
|
de: 'Deutsche Ausschreibungen automatisch erfassen und durchsuchen',
|
|
en: 'Automatically track and search German public tenders',
|
|
},
|
|
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,
|
|
},
|
|
});
|
|
}
|