test(10-03): add failing specs + real DÖE fixtures for adapter/normalizer

- Capture real, trimmed DÖE day-export fixtures (pubDay=2026-07-19, 8
  notices spanning tender/award/planning/untagged-with-awards tag classes)
  under tenders/__fixtures__/, live-downloaded from oeffentlichevergabe.de
- Define RawTenderRecord/NormalizedTenderFields/SourceType (tender.types.ts)
  and TenderSourceAdapter (day-cursor fetchTenders signature per RESEARCH
  Pattern 1)
- RED: doe-opendata.adapter.spec.ts asserts D-02 exact-count filtering,
  HTTP-400 no-op, and a zip-bomb ceiling guard (T-10-07)
- RED: tender-normalizer.service.spec.ts asserts eForms-primary deadline
  recovery where OCDS is null (RESEARCH Pattern 3), nullable deadline/value,
  dedupKey priority (ocid -> sourcePortal:sourceNoticeId), and a stable
  sha256 contentHash
This commit is contained in:
2026-07-21 10:56:13 +02:00
parent f80d491adc
commit f88e2a8141
6 changed files with 388 additions and 0 deletions
@@ -0,0 +1,29 @@
import type { RawTenderRecord, SourceType } from '../tender.types';
/**
* Contract implemented by every tender source adapter (DÖE OpenData this
* phase; AI-NetServer/cosinex/RSS/email-alert in later phases).
*
* `fetchTenders` takes a day-cursor string (`YYYY-MM-DD`), NOT a `since: Date`
* timestamp — the DÖE OpenData API is a daily batch-export API with no
* incremental/since parameter (RESEARCH.md Pattern 1 corrects the
* `ARCHITECTURE.md` sketch's `since?: Date` signature). Day-cursor is kept
* as the shared interface shape so future adapters that ARE incremental
* (e.g. RSS) can still satisfy it by treating the day as a coarse filter,
* rather than forking the interface per source.
*/
export interface TenderSourceAdapter {
readonly sourceType: SourceType;
/**
* Fetch and lightly parse all open-tender ("tender"-tagged", D-02)
* notices for the given calendar day (Europe/Berlin), already filtered —
* implementations must NOT return award/planning/untagged-with-awards
* notices.
*
* @param dayCursor - `YYYY-MM-DD`, must be strictly in the past (today/
* future is rejected upstream by the DÖE API with HTTP 400). Implementations
* treat that 400 as an expected no-op and resolve to `[]`, never throw.
*/
fetchTenders(dayCursor: string): Promise<RawTenderRecord[]>;
}