13 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 10-ausschreibungs-radar-foundation-d-e-ingestion | 01 | execute | 1 |
|
false |
|
|
Phase Goal (user story)
As a Tessera-Administrator, I want to das Ausschreibungs-Radar-Modul im Marketplace aktivieren und automatisch normalisierte deutsche DÖE-Ausschreibungen auf einem gemeinsamen, mandantensicheren Zeitplan erfassen lassen, so that alle Mandanten aktuelle, bietbare Vergaben durchsuchen koennen, ohne dass pro Mandant redundant gepollt wird.
Purpose: Every later slice (module registration, adapter, normalizer, ingestion, scheduler) depends on these tables and packages existing. The global-not-tenant-scoped shape of Tender (D-03) is the single highest-leverage decision and is locked in here.
Output: Migrated Tender + TenderSourcePollConfig tables, generated Prisma client types, three installed packages.
<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-RESEARCH.md @.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-PATTERNS.md @.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-CONTEXT.md @apps/api/prisma/schema.prisma @apps/api/prisma/migrations/20260714090000_add_ldap_user_exclude_list/migration.sqlArtifacts This Phase Produces (whole-phase reference)
Prisma models: Tender, TenderSourcePollConfig (Plan 01).
NestJS classes: TendersModule, seedTendersModule() (Plan 02); DoeOpenDataAdapter, TenderSourceAdapter interface, TenderNormalizerService (Plan 03); TenderIngestionService, TenderSchedulerService (Plan 04); TendersController, SourceConfigDto, TenderQueryDto (Plan 05).
Types: RawTenderRecord, NormalizedTenderFields, SourceType in tenders/tender.types.ts (Plan 03).
Scheduler methods: TenderSchedulerService.setInterval(intervalMin), stopJob(); TenderIngestionService.pollDueSources(), pruneExpiredTenders() (Plan 04).
Config keys / slugs: module slug tender-radar; TenderSourcePollConfig.sourceType = 'doe-opendata' (singleton).
New file paths: apps/api/src/tenders/**, apps/web/src/app/(portal)/modules/tender-radar/page.tsx, apps/web/src/lib/module-loader.ts (+tender-radar entry), fixture ZIPs under apps/api/src/tenders/__fixtures__/.
Add two models to `apps/api/prisma/schema.prisma` following the 10-PATTERNS.md schema section exactly:
`Tender` — global reference table, deliberately NO `tenantId` and NO `@@index([tenantId])` (D-03: platform-global data). Fields: `id String @id @default(uuid())`, `sourcePortal String`, `sourceNoticeId String`, `ocid String?`, `dedupKey String @unique`, `title String`, `buyerName String?`, `cpvCodes String[] @default([])`, `region String?`, `plz String?`, `bundesland String?`, `deadlineAt DateTime?` (frequently null per RESEARCH Pattern 4 — nullable is mandatory, not optional hardening), `estimatedValue Decimal? @db.Decimal(14,2)`, `procedureType String?`, `status String @default("active")` (values `active` | `expired`; supports D-05 retention marking), `sourceUrl String?`, `contentHash String` (SCHEMA-02 change detection), `rawPayload Json?` (debugging/re-normalization), `publishedAt DateTime`, `createdAt DateTime @default(now())`, `updatedAt DateTime @updatedAt`. Indexes: `@@index([status])`, `@@index([deadlineAt])`, `@@index([publishedAt])`.
`TenderSourcePollConfig` — singleton-per-source admin config (INGEST-06 foundation). Fields: `id String @id @default(uuid())`, `sourceType String @unique` (fixed slug, `doe-opendata` — the `@unique` makes the singleton intent explicit, mirroring DkvModuleConfig.tenantId @unique), `pollIntervalMin Int @default(60)` (D-04 default hourly), `isActive Boolean @default(false)`, `lastIngestedDay DateTime?` (day-cursor, NOT a timestamp — RESEARCH Pattern 1), `createdAt DateTime @default(now())`, `updatedAt DateTime @updatedAt`.
Write the handwritten migration at `apps/api/prisma/migrations/20260721120000_add_tender_radar/migration.sql` mirroring the ldap-user-exclude-list migration style: `CREATE TABLE IF NOT EXISTS "Tender" (...)` and `CREATE TABLE IF NOT EXISTS "TenderSourcePollConfig" (...)` with matching column types, plus `CREATE UNIQUE INDEX IF NOT EXISTS` on `Tender.dedupKey` and `TenderSourcePollConfig.sourceType`, and the three `Tender` indexes.
Run `pnpm --filter @tessera/api exec prisma generate` so the Prisma client TS types for `Tender`/`TenderSourcePollConfig` are available to downstream service tasks.
cd apps/api && grep -c "model Tender" prisma/schema.prisma && grep -c "model TenderSourcePollConfig" prisma/schema.prisma && test -f prisma/migrations/20260721120000_add_tender_radar/migration.sql && pnpm exec tsc --noEmit -p tsconfig.json 2>&1 | tail -5
- `grep -c "model Tender"` and `grep -c "model TenderSourcePollConfig"` in schema.prisma each return >= 1.
- Migration SQL file exists at the timestamped path.
- `Tender` has no `tenantId` column (assert: `grep -c "tenantId" ` within the Tender model block returns 0).
- `prisma generate` succeeds and `import { Tender } from '@prisma/client'` type-checks.
Both models present with correct fields, migration SQL written, Prisma client regenerated, packages installed.
Task 3: [BLOCKING] Apply the tender-radar migration to the database
- apps/api/prisma/migrations/migration_lock.toml (confirms postgresql provider + migrate workflow)
- apps/api/prisma/migrations/20260721120000_add_tender_radar/migration.sql (the migration written in Task 2)
apps/api/prisma/migrations/20260721120000_add_tender_radar/migration.sql
[BLOCKING] Apply the handwritten migration to the live dev database so the Prisma client's compile-time types are backed by real tables (build/type checks pass WITHOUT this step because types come from the generated client, not the DB — this creates a false-positive verification state, so this task is mandatory before any ingestion/persistence verification).
Run `pnpm --filter @tessera/api exec prisma migrate deploy` (non-interactive; applies the committed handwritten migration). If `migrate deploy` reports drift or a non-empty schema conflict that requires interactive resolution, fall back to `pnpm --filter @tessera/api exec prisma db push` (schema is additive — two new tables, no destructive change expected). Then confirm the tables exist.
cd apps/api && pnpm exec prisma migrate status 2>&1 | tail -8
- `prisma migrate status` reports the database schema is up to date (no pending migrations), OR a follow-up `prisma db push` reports "already in sync".
- Tables `Tender` and `TenderSourcePollConfig` exist in the database (verifiable via `prisma migrate status` clean state or a `prisma.tender.count()` call returning 0 without error).
Migration applied; both tables live in the database; no pending migrations.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| npm registry → build | Third-party package code (adm-zip) enters the trusted build via pnpm add |
| ORM → Postgres | New global tables added; tenant-isolation design decision is encoded in schema shape |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-10-SC | Tampering | pnpm add adm-zip (supply chain) |
high | mitigate | Task 1 blocking-human checkpoint verifies adm-zip on npmjs.com + github before install; fast-xml-parser/csv-parse pre-approved in RESEARCH |
| T-10-01 | Information Disclosure | Tender table schema |
high | mitigate | Tender carries NO tenantId by design (D-03) — it is platform-global reference data; per-tenant scoping lives one layer up (Phase 11 saved searches). Encoding this now prevents a later blanket where: { tenantId } habit from silently hiding global data from a second tenant |
| T-10-02 | Denial of Service | Decimal/String[] columns |
low | accept | Standard Postgres column types; ingestion-side size guards handled in Plan 03 (zip-bomb) — no DB-level risk introduced by the schema itself |
| </threat_model> |
<success_criteria>
- SCHEMA-01 data foundation: normalized OCDS-oriented
Tendertable exists with nullable deadline/value. - Packages installed; migration applied; client regenerated. </success_criteria>