| 10-ausschreibungs-radar-foundation-d-e-ingestion |
03 |
api |
| doe-opendata |
| adm-zip |
| fast-xml-parser |
| eforms |
| ocds |
| tdd |
| normalizer |
| ingestion |
|
| phase |
provides |
| 10-01 (foundation & dependencies) |
Tender/TenderSourcePollConfig Prisma models, fast-xml-parser/adm-zip/csv-parse installed |
|
| phase |
provides |
| 10-02 (marketplace registration) |
TendersModule skeleton with empty providers array, ready for adapter/normalizer wiring |
|
|
| DoeOpenDataAdapter — fetches, extracts, and D-02-filters the DÖE day-batch export into RawTenderRecord[] (INGEST-01) |
| TenderNormalizerService — maps RawTenderRecord to Tender fields, dedupKey, contentHash (SCHEMA-01) |
| Real, trimmed DÖE fixture ZIPs (doe-eforms-sample.zip / doe-ocds-sample.zip) for future ingestion-service tests |
| RawTenderRecord/NormalizedTenderFields/SourceType shared types + TenderSourceAdapter interface (day-cursor signature) |
|
| 10-04 ingestion/scheduler (consumes fetchTenders + normalize) |
| 10-05 controller/DTOs |
| phase 11 saved searches/filter UI |
|
| added |
patterns |
|
|
| Day-cursor adapter contract (fetchTenders(dayCursor: string)) — not since:Date — per DÖE's daily-batch-only API shape |
| eForms-DE XML as PRIMARY source for deadlineAt/estimatedValue/procedureType; OCDS PRIMARY for ocid/buyerName/title/cpvCodes/region/plz (RESEARCH Pattern 3 reversal of ARCHITECTURE.md) |
| Pre-extraction decompression-bomb ceiling check (sum entry.header.size before any getData() call) — adm-zip getEntries() reads only the central directory |
| Positive tag-presence D-02 filter (tag.includes('tender')) — missing/other tags conservatively excluded, never default-open |
| Pure normalizer service (no I/O), module-level extraction helpers, single normalize() entry point — mirrors DkvParserService shape |
|
|
| created |
modified |
| apps/api/src/tenders/__fixtures__/doe-eforms-sample.zip |
| apps/api/src/tenders/__fixtures__/doe-ocds-sample.zip |
| apps/api/src/tenders/tender.types.ts |
| apps/api/src/tenders/adapters/tender-source-adapter.interface.ts |
| apps/api/src/tenders/adapters/doe-opendata.adapter.ts |
| apps/api/src/tenders/adapters/doe-opendata.adapter.spec.ts |
| apps/api/src/tenders/tender-normalizer.service.ts |
| apps/api/src/tenders/tender-normalizer.service.spec.ts |
|
| apps/api/src/tenders/tenders.module.ts |
|
|
| Fixtures captured LIVE from oeffentlichevergabe.de (pubDay=2026-07-19, not synthetic) — 8 real notices trimmed from a 594-notice day, spanning all four D-02 tag classes (tender/award/planning/untagged-with-awards) plus one directly cross-checked eForms-primary-deadline pair, per this repo's real-fixture precedent (DKV PDF parser) |
| sourceNoticeId = OCDS release.id (stable, no version suffix), NOT the zip entry filename (which carries a -NN version suffix) — matches the dedupKey fallback tier's intent of a stable per-notice identifier |
| Ceiling check runs per-archive, sequentially (eforms.zip checked+extracted before ocds.zip is even fetched) — a bomb in the first archive short-circuits before a second network call is made |
| bundesland (Bundesland name) intentionally left null — NUTS-code-to-Bundesland-name mapping is deferred to Phase 11's filter UI, out of this phase's ingestion-core scope |
| XMLParser configured with removeNSPrefix:true — eForms-DE's cac:/cbc:/efac: namespace prefixes are stripped so normalizer code addresses tags by local name only (ContractNotice.ProcurementProjectLot[0].TenderingProcess.TenderSubmissionDeadlinePeriod.EndDate) |
|
|
| id |
description |
requirement |
verification |
human_judgment |
| D1 |
DoeOpenDataAdapter fetches a day's DÖE export ZIP, extracts it, and parses eForms-DE XML (primary) + OCDS JSON (ocid) into RawTenderRecord[] (INGEST-01) |
INGEST-01 |
| kind |
ref |
status |
| unit |
apps/api/src/tenders/adapters/doe-opendata.adapter.spec.ts — 6/6 green: sourceType, parse+pair into RawTenderRecord[], D-02 filter, 400-no-op, zip-bomb ceiling, no-axios |
pass |
|
|
false |
|
| id |
description |
requirement |
verification |
human_judgment |
| D2 |
Only open tenders survive the D-02 filter: tag=['tender'] included; award/planning/untagged-with-awards excluded, exact retained count proven against real fixture |
INGEST-01 |
| kind |
ref |
status |
| unit |
doe-opendata.adapter.spec.ts — fetchTenders() on the 8-notice real fixture (4 tender / 2 award / 1 planning / 1 untagged-with-awards) returns exactly 4 records; explicit not.toContain assertions for each excluded notice id |
pass |
|
|
false |
|
| id |
description |
requirement |
verification |
human_judgment |
| D3 |
TenderNormalizer maps a real eForms+OCDS notice pair into Tender fields with nullable deadline/value, a stable dedupKey (ocid -> sourcePortal:noticeId), and a contentHash (SCHEMA-01) |
SCHEMA-01 |
| kind |
ref |
status |
| unit |
tender-normalizer.service.spec.ts — 6/6 green: eForms-only deadline recovery (real cross-checked pair, OCDS tenderPeriod absent), missing-deadline/value -> null, dedupKey=ocid, dedupKey fallback, contentHash stability, contentHash changes on deadline mutation |
pass |
|
|
false |
|
| id |
description |
verification |
human_judgment |
| D4 |
Zip-slip / decompression-bomb safety present in the extract path (T-10-07) |
| kind |
ref |
status |
| unit |
doe-opendata.adapter.spec.ts — 60MB-declared synthetic archive (highly compressible, tiny on disk) rejected before any entry.getData() call; entries never written to disk (zip-slip structurally absent, documented in code) |
pass |
|
|
false |
|
|
~35min |
2026-07-21 |
complete |