Files
tessera-ctl/.planning/phases/13-scraping-adapters-cross-source-dedup/13-06-SUMMARY.md
T

6.8 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions requirements-completed coverage duration completed status
13-scraping-adapters-cross-source-dedup 06 api
nestjs
prisma
react
tender-radar
dedup
phase provides
13-01 TenderSource model (sourcePortal, sourceUrl, sourceNoticeId) + backfill of existing DÖE tenders
phase provides
13-03 dedup-resolver fan-out wiring that attaches additional TenderSource rows to an existing Tender
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
14-i18n-admin-source-ui
tender-radar-detail-view
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.
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
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.
SCHEMA-03
id description requirement verification human_judgment
D1 GET /modules/tender-radar/:id includes the TenderSource relation (sourcePortal, sourceUrl, sourceNoticeId) via Prisma include, without changing route order or tenant scoping. SCHEMA-03
kind ref status
unit 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) pass
false
id description requirement verification human_judgment
D2 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. SCHEMA-03
kind ref status
unit apps/web/.../TenderDetail.test.tsx#renders a link for every source when tender.sources[] has multiple entries (13-06, SCHEMA-03) pass
kind ref status
unit apps/web/.../TenderDetail.test.tsx#falls back to the single sourceUrl block when tender.sources is missing/empty pass
false
15min 2026-07-23 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 <ul> of source links with hardcoded German portal labels (DÖE, tender24, lhs-vpbw, vergabe.landbw, DTVP), falling back to the pre-existing single sourceUrl block when sources is missing or empty (older tenders / responses without the include)

Task Commits

Each task was committed atomically:

  1. Task 1: getTender liefert sources[] via include - 166194f (feat)
  2. Task 2: API-Client-Typ + TenderDetail Multi-Source-Liste - bf61316 (feat)

Plan metadata: pending (this commit)

Files Created/Modified

  • apps/api/src/tenders/tenders.controller.ts - getTender now does prisma.tender.findUnique({ where, include: { sources: { select: {...} } } })
  • apps/api/src/tenders/tenders.controller.spec.ts - fake prisma tender t1 now carries a two-entry sources array; new test asserts the exact include/select shape and the passthrough response
  • apps/web/src/lib/tender-radar-api.ts - Tender.sources? optional field added, documented as detail-only/optional
  • apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.tsx - added portalLabel() map + multi-source <ul> rendering, with fallback to the original single-sourceUrl block
  • apps/web/src/app/(portal)/modules/tender-radar/components/TenderDetail.test.tsx - two new tests: multi-link rendering with portal labels, and fallback-to-single-link when sources is empty/missing

Decisions Made

  • Kept portalLabel() as a local, non-exported function in TenderDetail.tsx rather than a shared module — only one consumer today; can be lifted to a shared constants file in Phase 14 (i18n rollout) if a second consumer appears.
  • Used select (not the full TenderSource row) in the Prisma include to avoid leaking id/tenderId/createdAt/ocid to the frontend — matches the existing minimal-projection convention (getCoverage's groupBy).

Deviations from Plan

None - plan executed exactly as written.

Issues Encountered

None.

User Setup Required

None - no external service configuration required.

Next Phase Readiness

  • Erfolgskriterium 3 (Anzeige aller Quell-Links) is now fully wired end-to-end: backend include (13-06) sits on top of the dedup-resolver fan-out (13-03) and the 13-01 backfill, so any tender that later gains a second TenderSource row (once a NetServer/cosinex adapter is active) will automatically surface a second link in the detail drawer with no further frontend work.
  • Admin-facing "excluded portals" UI (UI-06) and i18n rollout remain explicitly out of scope here, deferred to Phase 14 per 13-CONTEXT.md.

Phase: 13-scraping-adapters-cross-source-dedup Completed: 2026-07-23

Self-Check: PASSED

All modified/created files verified present on disk; both task commits (166194f, bf61316) verified present in git history.