--- phase: 10-ausschreibungs-radar-foundation-d-e-ingestion plan: 02 subsystem: api tags: [nestjs, module-registry, prisma, next.js, module-loader, tender-radar, marketplace] # Dependency graph requires: - phase: 10-01 (foundation & dependencies) provides: Tender / TenderSourcePollConfig Prisma models, applied migration provides: - Self-seeding TendersModule registered in app.module.ts imports (marketplace visibility, CONFIG-01) - Singleton doe-opendata TenderSourcePollConfig row upserted idempotently on boot - Web MODULE_REGISTRY['tender-radar'] whitelist entry + minimal placeholder page - Unit-test coverage proving the registry seed call shape affects: [10-03 adapter/normalizer, 10-04 ingestion/scheduler, 10-05 controller/DTOs, phase 11 saved searches UI] # Tech tracking tech-stack: added: [] patterns: - "Self-seeding NestJS module via OnModuleInit (mirrors DkvModule/CertManagerModule)" - "Singleton platform-global config upsert (sourceType @unique) inside module bootstrap, no forTenant()" - "Frontend module-loader security whitelist entry required alongside DB seed for a module's page to resolve" key-files: created: - apps/api/src/tenders/tenders.module.ts - apps/api/src/tenders/tenders.seed.ts - apps/api/src/tenders/tenders.seed.spec.ts - apps/web/src/app/(portal)/modules/tender-radar/page.tsx modified: - apps/api/src/app.module.ts - apps/web/src/lib/module-loader.ts key-decisions: - "category: 'procurement' chosen for the marketplace card — free-form kebab-case per existing seed convention (dkv='fleet', cert-manager='security-tools'), no enum constraint in seedModule or the marketplace UI" - "Singleton doe-opendata poll config upserted directly inside TendersModule.onModuleInit() via injected PrismaService, not via a separate ingestion service (Plans 03-04 add ingestion machinery, not this plan)" - "isActive: true by default on the singleton so the platform-global DÖE poll runs regardless of any tenant's per-tenant activation state (D-04); day-cursor gating (Plan 04) makes ticks idempotent before that logic exists" - "Placeholder page.tsx uses a hardcoded German string, not next-intl — full i18n is CONFIG-03 (Phase 14); documented inline as an intentional MVP stub to be replaced in Phase 11" requirements-completed: [CONFIG-01] coverage: - id: D1 description: "TendersModule self-seeds 'tender-radar' into the module registry on boot (isSystem: true, bilingual description) so an admin can find it in the marketplace, same as DKV/Cert-Manager" requirement: "CONFIG-01" verification: - kind: unit ref: "apps/api/src/tenders/tenders.seed.spec.ts#calls moduleRegistryService.seedModule once with tender-radar slug" status: pass - kind: integration ref: "cd apps/api && pnpm exec tsc --noEmit -p tsconfig.json; grep -c TendersModule src/app.module.ts == 2" status: pass human_judgment: false - id: D2 description: "Activating the module opens /modules/tender-radar instead of 404, because the module-loader whitelist entry exists" requirement: "CONFIG-01" verification: - kind: integration ref: "cd apps/web && grep -c tender-radar src/lib/module-loader.ts >=1; test -f 'src/app/(portal)/modules/tender-radar/page.tsx'; pnpm exec tsc --noEmit" status: pass human_judgment: true rationale: "Whitelist entry and file existence are statically verified, but actually clicking Activate in the marketplace and confirming the page renders (rather than 404s) in a live browser session was not exercised in this run — recommend a quick UAT pass alongside Plan 03-05 verification." - id: D3 description: "Singleton doe-opendata TenderSourcePollConfig row is seeded exactly once per boot (idempotent upsert), no per-tenant duplication possible (sourceType @unique)" requirement: "CONFIG-01" verification: - kind: unit ref: "schema.prisma sourceType @unique constraint (10-01) + upsert-by-sourceType in tenders.module.ts onModuleInit" status: pass human_judgment: true rationale: "DB-level uniqueness and upsert code are statically verifiable; actually restarting the live API container twice and confirming the row count stays at 1 was not exercised in this run (local Docker deploy/restart is left to the user per project convention)." # Metrics duration: ~20min completed: 2026-07-21 status: complete --- # Phase 10 Plan 02: Ausschreibungs-Radar Marketplace Registration Summary **Self-seeding `TendersModule` (slug `tender-radar`, `isSystem: true`) registered in `app.module.ts`, singleton `doe-opendata` poll config upserted on boot, and a whitelisted `/modules/tender-radar` placeholder page — the first user-facing vertical slice of CONFIG-01.** ## Performance - **Duration:** ~20 min - **Started:** 2026-07-21 - **Completed:** 2026-07-21 - **Tasks:** 3 (all auto, no checkpoints) - **Files modified:** 6 (3 created API, 1 created web, 2 modified) ## Accomplishments - `TendersModule` self-seeds the marketplace registry via `seedTendersModule()`, mirroring the exact `DkvModule`/`CertManagerModule` `OnModuleInit` pattern (self-seed in try/catch with Logger). - Singleton `doe-opendata` `TenderSourcePollConfig` row is upserted directly in `TendersModule.onModuleInit()` using the injected (non-tenant-extended) `PrismaService` — no `forTenant()`, matching the D-03 global-data invariant established in Plan 01. - `TendersModule` added to `app.module.ts` imports, alongside `DkvModule`/`CertManagerModule`/`FavoritesModule`. - Web `MODULE_REGISTRY['tender-radar']` whitelist entry added — mandatory per the module-loader's own security-whitelist comment (arbitrary slugs 404 without it, even when activated in the DB). - Minimal `tender-radar/page.tsx` client component created — proves the activation → lazy-load → page-render path end-to-end; real results UI is deferred to Phase 11. - Unit test (`tenders.seed.spec.ts`) asserts the registry seed call shape (slug, `isSystem: true`, bilingual `de`/`en` description) — Wave 0 automated proof for CONFIG-01. ## Task Commits Each task was committed atomically: 1. **Task 1: TendersModule skeleton + registry self-seed + singleton poll config** - `e7b4094` (feat) 2. **Task 2: Web module-loader whitelist entry + minimal portal page** - `3292afb` (feat) 3. **Task 3: Seed registration test (Wave 0 coverage for CONFIG-01)** - `9a7ed71` (test) **Plan metadata:** see final `docs(10-02)` commit. ## Files Created/Modified - `apps/api/src/tenders/tenders.seed.ts` - exports `seedTendersModule()`, upserts `tender-radar` into the registry (`isSystem: true`, category `procurement`, bilingual description) - `apps/api/src/tenders/tenders.module.ts` - `TendersModule` with `OnModuleInit`; calls `seedTendersModule()` and upserts the singleton `doe-opendata` poll config (both wrapped in independent try/catch + Logger) - `apps/api/src/tenders/tenders.seed.spec.ts` - Vitest coverage for the seed call shape - `apps/api/src/app.module.ts` - `TendersModule` added to imports - `apps/web/src/lib/module-loader.ts` - `'tender-radar'` entry added to `MODULE_REGISTRY` - `apps/web/src/app/(portal)/modules/tender-radar/page.tsx` - minimal placeholder page (hardcoded German string, `'use client'`, default export) ## Decisions Made - **`category: 'procurement'`** — confirmed free-form (no enum constraint in `seedModule()` or the marketplace filter UI, which derives categories via `[...new Set(modules.map(m => m.category))]`); follows existing kebab-case-ish convention (`fleet`, `security-tools`, `domain-tools`). - **Poll config upsert lives in the module, not a service** — Plans 03-04 haven't added `TenderIngestionService` yet; the plan explicitly scoped the singleton seed to `TendersModule.onModuleInit()` via injected `PrismaService` for this wave. - **`isActive: true` by default** — per plan instruction (D-04): the platform-global poll should run regardless of tenant activation state; idempotency is guaranteed by `sourceType @unique` plus the (future) day-cursor gate. - **Hardcoded German placeholder string** — explicit MVP stub per plan instruction; not wired to next-intl since full i18n is CONFIG-03 (Phase 14). Documented inline in the page component's doc comment so Phase 11 authors know to replace the whole component rather than just the string. ## Deviations from Plan None - plan executed exactly as written. ## Issues Encountered None. ## Known Stubs - `apps/web/src/app/(portal)/modules/tender-radar/page.tsx` - Hardcoded German placeholder text ("Ausschreibungen werden erfasst.") with no real data source wired. This is an intentional, explicitly-planned MVP stub (plan Task 2 action text) proving the activation → page-load path only. Resolved by Phase 11 (results/search UI) and Phase 14 (full i18n via next-intl message keys). ## User Setup Required None - no external service configuration required. Local Docker stack (`tessera-ctl-api-1`) was left running unmodified; restarting/rebuilding it to pick up these code changes is left to the user per project convention. ## Next Phase Readiness - CONFIG-01 vertical slice complete: module seeds into the registry, is admin-activatable via the existing Marketplace mechanism (`ModuleRegistryService`/`TenantModuleActivation`, unchanged), and its page resolves once the whitelist entry is registered. - `TendersModule` is a clean extension point: `controllers: []` and `providers: []` are placeholders ready for Plan 05 (controller/DTOs) and Plans 03-04 (adapter/normalizer/scheduler/ingestion services) to populate. - No blockers for Plan 10-03. ## Self-Check: PASSED - FOUND: apps/api/src/tenders/tenders.module.ts - FOUND: apps/api/src/tenders/tenders.seed.ts - FOUND: apps/api/src/tenders/tenders.seed.spec.ts - FOUND: apps/web/src/lib/module-loader.ts - FOUND: apps/web/src/app/(portal)/modules/tender-radar/page.tsx - FOUND: apps/api/src/app.module.ts - FOUND commit: e7b4094 - FOUND commit: 3292afb - FOUND commit: 9a7ed71 --- *Phase: 10-ausschreibungs-radar-foundation-d-e-ingestion* *Completed: 2026-07-21*