--- phase: 13-scraping-adapters-cross-source-dedup plan: 06 subsystem: api tags: [nestjs, prisma, react, tender-radar, dedup] # Dependency graph requires: - phase: 13-01 provides: TenderSource model (sourcePortal, sourceUrl, sourceNoticeId) + backfill of existing DÖE tenders - phase: 13-03 provides: dedup-resolver fan-out wiring that attaches additional TenderSource rows to an existing Tender provides: - GET /modules/tender-radar/:id now includes the sources[] relation (TenderSource rows) - Tender API client type carries optional sources[] (sourcePortal, sourceUrl, sourceNoticeId) - TenderDetail renders one link per source portal with a German portal label, falling back to the single sourceUrl block when sources is missing/empty affects: [14-i18n-admin-source-ui, tender-radar-detail-view] # Tech tracking tech-stack: added: [] patterns: - "Read-surface include: TenderSource is exposed via Prisma `include` with a minimal `select` (sourcePortal/sourceUrl/sourceNoticeId), same minimal-projection convention as getCoverage's groupBy." - "Optional relation field on frontend API types (`sources?: [...]`) with an explicit empty/missing fallback to the legacy single-field UI block — safe rollout path for a new relation without breaking older/undeployed callers." key-files: created: [] modified: - apps/api/src/tenders/tenders.controller.ts - apps/api/src/tenders/tenders.controller.spec.ts - apps/web/src/lib/tender-radar-api.ts - apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.tsx - apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.test.tsx key-decisions: - "portalLabel() map lives inline in TenderDetail.tsx (not a shared constant) — only this component needs it today; unknown sourcePortal slugs fall back to the raw slug so a future portal never silently disappears from the list." - "Fallback order is sources[]-non-empty -> legacy sourceUrl block -> 'Kein Quell-Link verfügbar' — preserves exact existing UI/tests for tenders that predate the TenderSource backfill or whose sources[] loads empty." requirements-completed: [SCHEMA-03] coverage: - id: D1 description: "GET /modules/tender-radar/:id includes the TenderSource relation (sourcePortal, sourceUrl, sourceNoticeId) via Prisma include, without changing route order or tenant scoping." requirement: "SCHEMA-03" verification: - kind: unit ref: "apps/api/src/tenders/tenders.controller.spec.ts#GET /:id includes the sources relation so the response carries all TenderSource links (13-06, SCHEMA-03)" status: pass human_judgment: false - id: D2 description: "TenderDetail renders a list of links (one per source portal, German label) when tender.sources[] has entries, and falls back to the existing single sourceUrl block when sources is missing or empty." requirement: "SCHEMA-03" verification: - kind: unit ref: "apps/web/.../TenderDetail.test.tsx#renders a link for every source when tender.sources[] has multiple entries (13-06, SCHEMA-03)" status: pass - kind: unit ref: "apps/web/.../TenderDetail.test.tsx#falls back to the single sourceUrl block when tender.sources is missing/empty" status: pass human_judgment: false # Metrics duration: 15min completed: 2026-07-23 status: complete --- # Phase 13 Plan 06: Multi-Source Read Surface Summary **GET /modules/tender-radar/:id now returns the tender's full TenderSource list and the detail drawer renders one clickable link per source portal (DÖE/tender24/DTVP/...), completing the display half of cross-source dedup (Erfolgskriterium 3).** ## Performance - **Duration:** ~15 min - **Tasks:** 2/2 completed - **Files modified:** 5 ## Accomplishments - `getTender` includes the `sources` relation (minimal `select`: sourcePortal, sourceUrl, sourceNoticeId), route order unchanged (`:id` still declared after all static routes) - `Tender` API client type gained an optional `sources[]` field mirroring the new backend shape - `TenderDetail` renders a `