Files

161 lines
9.9 KiB
Markdown

---
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*