163 lines
11 KiB
Markdown
163 lines
11 KiB
Markdown
---
|
|
phase: 10-ausschreibungs-radar-foundation-d-e-ingestion
|
|
plan: 02
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["10-01"]
|
|
files_modified:
|
|
- apps/api/src/tenders/tenders.module.ts
|
|
- apps/api/src/tenders/tenders.seed.ts
|
|
- apps/api/src/tenders/tenders.seed.spec.ts
|
|
- apps/api/src/app.module.ts
|
|
- apps/web/src/lib/module-loader.ts
|
|
- apps/web/src/app/(portal)/modules/tender-radar/page.tsx
|
|
autonomous: true
|
|
requirements: [CONFIG-01]
|
|
must_haves:
|
|
truths:
|
|
- "Admin can find 'Ausschreibungs-Radar' in the marketplace and activate it for a tenant, same as DKV/Cert-Manager (CONFIG-01)"
|
|
- "Activating the module for a tenant opens the module page (no 404) because the module-loader whitelist entry exists"
|
|
- "The singleton 'doe-opendata' TenderSourcePollConfig row is seeded so the shared poll is admin-drivable"
|
|
artifacts:
|
|
- apps/api/src/tenders/tenders.module.ts
|
|
- apps/api/src/tenders/tenders.seed.ts
|
|
- apps/web/src/lib/module-loader.ts
|
|
- apps/web/src/app/(portal)/modules/tender-radar/page.tsx
|
|
key_links:
|
|
- "TendersModule.onModuleInit() calls seedTendersModule() → ModuleRegistryService.seedModule({ slug: 'tender-radar', isSystem: true })"
|
|
- "module-loader.ts MODULE_REGISTRY['tender-radar'] entry is MANDATORY — page 404s without it even when activated"
|
|
---
|
|
|
|
<objective>
|
|
Deliver the first user-facing vertical slice: register the Ausschreibungs-Radar module in the marketplace (`isSystem: true`, admin-activatable per tenant) and make its portal page load when activated. Seed the singleton `doe-opendata` poll-config row so later plans have a config to drive.
|
|
|
|
## Phase Goal (user story)
|
|
**As a** Tessera-Administrator, **I want to** das Ausschreibungs-Radar-Modul im Marketplace aktivieren, **so that** ich Zugang zum Ausschreibungs-Katalog fuer einen Mandanten freischalten kann — genau wie beim DKV-Fleet- und Cert-Manager-Modul (CONFIG-01).
|
|
|
|
Purpose: After this plan a real admin can DO something new: find the module in the marketplace, activate it for a tenant, and open its page. This is the CONFIG-01 slice end-to-end (registry seed → marketplace → activation → lazy-loaded page).
|
|
Output: Self-seeding `TendersModule`, module-loader whitelist entry, minimal placeholder page, seeded singleton poll config.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-PATTERNS.md
|
|
@apps/api/src/dkv/dkv.module.ts
|
|
@apps/api/src/cert-manager/cert-manager.seed.ts
|
|
@apps/api/src/module-registry/module-registry.service.ts
|
|
@apps/web/src/lib/module-loader.ts
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: TendersModule skeleton + registry self-seed + singleton poll config</name>
|
|
<read_first>
|
|
- apps/api/src/dkv/dkv.module.ts (exact OnModuleInit self-seed pattern to copy)
|
|
- apps/api/src/cert-manager/cert-manager.seed.ts (preferred seed analog — isSystem:true + "admin must activate via Marketplace" phrasing matches CONFIG-01)
|
|
- apps/api/src/module-registry/module-registry.service.ts (seedModule upsert-by-slug signature, lines ~158-187)
|
|
- apps/api/src/app.module.ts (imports array — add TendersModule alongside DkvModule/CertManagerModule)
|
|
</read_first>
|
|
<files>apps/api/src/tenders/tenders.module.ts, apps/api/src/tenders/tenders.seed.ts, apps/api/src/app.module.ts</files>
|
|
<action>
|
|
Create `tenders.seed.ts` exporting `seedTendersModule(moduleRegistryService)` that calls `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 })` — mirror `cert-manager.seed.ts` verbatim in structure (CONFIG-01). Confirm `procurement` is acceptable as a new category value (seedModule does not constrain category); if the marketplace UI needs a known category, reuse an existing one and note it in the summary.
|
|
|
|
Create `tenders.module.ts` implementing `OnModuleInit` exactly like `DkvModule`: `imports: [ModuleRegistryModule]`, `controllers: []` (added Plan 05), `providers: []` (services added Plans 03-04), and an `onModuleInit()` that calls `seedTendersModule(this.moduleRegistryService)` inside try/catch with a Logger. Also, in `onModuleInit()`, upsert the singleton poll config: `prisma.tenderSourcePollConfig.upsert({ where: { sourceType: 'doe-opendata' }, update: {}, create: { sourceType: 'doe-opendata', pollIntervalMin: 60, isActive: true } })` — inject `PrismaService` (PrismaModule is global). isActive defaults true so the platform-global DÖE poll (D-04 hourly) runs regardless of tenant activation; the day-cursor gate (Plan 04) makes this idempotent. Do NOT use `forTenant()` — this config and the Tender table are global (D-03).
|
|
|
|
Add `TendersModule` to the `imports` array in `app.module.ts`.
|
|
</action>
|
|
<verify>
|
|
<automated>cd apps/api && pnpm exec tsc --noEmit -p tsconfig.json 2>&1 | tail -5 && grep -c "TendersModule" src/app.module.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `tenders.seed.ts` seeds slug `tender-radar` with `isSystem: true`.
|
|
- `TendersModule` is imported in `app.module.ts` (`grep -c "TendersModule" src/app.module.ts` >= 2 — import line + array entry).
|
|
- Singleton `doe-opendata` config upserted on init (idempotent — re-boot does not create a second row; guaranteed by `sourceType @unique`).
|
|
- `tsc --noEmit` passes.
|
|
</acceptance_criteria>
|
|
<done>Module self-seeds into the registry on boot and seeds the singleton poll config; app compiles.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Web module-loader whitelist entry + minimal portal page</name>
|
|
<read_first>
|
|
- apps/web/src/lib/module-loader.ts (MODULE_REGISTRY object — copy the 'cert-manager' entry shape)
|
|
- apps/web/src/app/(portal)/modules/cert-manager/page.tsx (page.tsx conventions: 'use client', useTranslations, default export)
|
|
</read_first>
|
|
<files>apps/web/src/lib/module-loader.ts, apps/web/src/app/(portal)/modules/tender-radar/page.tsx</files>
|
|
<action>
|
|
Add a `'tender-radar'` entry to `MODULE_REGISTRY` in `module-loader.ts` pointing at `dynamic(() => import('@/app/(portal)/modules/tender-radar/page'), { ssr: false })` — identical shape to the existing `'cert-manager'`/`'dkv-fleet'` entries. This whitelist entry is MANDATORY for CONFIG-01: the page 404s even when the module is activated if the slug is not whitelisted (security-motivated whitelist, module-loader header comment).
|
|
|
|
Create a minimal `tender-radar/page.tsx` client component (default export, `'use client'`) that renders the module title and a short "Ausschreibungen werden erfasst" placeholder. Real results UI is Phase 11 — this is the thinnest page that proves the activation → page-load path. Use a hardcoded German string here (full i18n is CONFIG-03, Phase 14); note this as an intentional MVP stub in the summary.
|
|
</action>
|
|
<verify>
|
|
<automated>cd apps/web && grep -c "tender-radar" src/lib/module-loader.ts && test -f "src/app/(portal)/modules/tender-radar/page.tsx" && pnpm exec tsc --noEmit 2>&1 | tail -5</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `MODULE_REGISTRY['tender-radar']` entry present (`grep -c "tender-radar" module-loader.ts` >= 1).
|
|
- `tender-radar/page.tsx` exists and default-exports a component.
|
|
- Web `tsc --noEmit` passes.
|
|
</acceptance_criteria>
|
|
<done>Activated module opens its page instead of 404; whitelist entry in place.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Seed registration test (Wave 0 coverage for CONFIG-01)</name>
|
|
<read_first>
|
|
- apps/api/src/cert-manager/cert-manager.service.spec.ts (existing vitest spec style in this repo)
|
|
- apps/api/src/tenders/tenders.seed.ts (unit under test, from Task 1)
|
|
</read_first>
|
|
<files>apps/api/src/tenders/tenders.seed.spec.ts</files>
|
|
<action>
|
|
Create `tenders.seed.spec.ts` (Vitest). Provide a mock `ModuleRegistryService` with a spied `seedModule`. Assert that `seedTendersModule(mock)` calls `seedModule` once with `slug: 'tender-radar'`, `isSystem: true`, and a description object containing both `de` and `en` keys. This is the Wave 0 automated proof for CONFIG-01 registration.
|
|
</action>
|
|
<verify>
|
|
<automated>cd apps/api && pnpm test -- tenders.seed 2>&1 | tail -15</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- Test asserts `seedModule` called with `slug: 'tender-radar'` and `isSystem: true`.
|
|
- `pnpm --filter @tessera/api test -- tenders.seed` passes.
|
|
</acceptance_criteria>
|
|
<done>Registration behaviour has automated coverage; green.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| tenant → module activation | A tenant's activation must gate only visibility, not create a second data copy or leak another tenant's config |
|
|
| URL slug → dynamic import | Only whitelisted slugs may resolve to a component (module-loader security whitelist) |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
|
| T-10-03 | Elevation of Privilege | `module-loader.ts` dynamic import | medium | mitigate | Only the fixed `'tender-radar'` slug is whitelisted; arbitrary URL slugs return null (existing whitelist mechanism, unchanged) |
|
|
| T-10-04 | Information Disclosure | per-tenant module activation | high | mitigate | Activation uses the existing `ModuleRegistryService` / `TenantModuleActivation` mechanism unchanged; the singleton `doe-opendata` config is global (one row, `sourceType @unique`) so a 2nd tenant activation cannot create or read a per-tenant duplicate. No `forTenant()` applied to the global config |
|
|
| T-10-05 | Tampering | registry self-seed on boot | low | accept | `seedModule` is an idempotent upsert-by-slug; repeated boots do not duplicate the module row |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- Module appears in the registry with slug `tender-radar` after boot (seed test green).
|
|
- Activating the module for a tenant opens `/modules/tender-radar` (whitelist entry present).
|
|
- Singleton poll config seeded exactly once (idempotent upsert).
|
|
- `pnpm --filter @tessera/api test` green; both apps `tsc --noEmit` clean.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- CONFIG-01: admin can find and activate Ausschreibungs-Radar in the marketplace, same as DKV/Cert-Manager, and open its page.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-02-SUMMARY.md` when done.
|
|
</output>
|