test(quick-260723-e7i): cover ai-netserver/cosinex bag normalization + DOE regression
- bagRecord() helper builds inline RawTenderRecords for the flat ocdsPayload bag shape (no fixtures exist for NetServer/cosinex-DTVP) - ai-netserver and cosinex-dtvp full-bag mapping, null/empty-field fallback, and contentHash-format assertions - Existing DOE fixture-based assertions untouched, confirming the refactor didn't change DOE-path behavior Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { XMLParser } from 'fast-xml-parser';
|
|||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { RawTenderRecord } from './tender.types';
|
import type { RawTenderRecord, SourceType } from './tender.types';
|
||||||
import { TenderNormalizerService } from './tender-normalizer.service';
|
import { TenderNormalizerService } from './tender-normalizer.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,6 +65,36 @@ const NOTICE_WITH_EFORMS_ONLY_DEADLINE =
|
|||||||
const NOTICE_WITH_NO_DEADLINE_OR_VALUE =
|
const NOTICE_WITH_NO_DEADLINE_OR_VALUE =
|
||||||
'001c68dd-258a-4bf7-a264-478604ad9c3e-01';
|
'001c68dd-258a-4bf7-a264-478604ad9c3e-01';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a RawTenderRecord for the flat ocdsPayload-bag scraper adapters
|
||||||
|
* (NetServer/cosinex-DTVP) — no fixture ZIPs exist for these sources, so
|
||||||
|
* the record is constructed inline (gap closure, 13-VERIFICATION.md).
|
||||||
|
*/
|
||||||
|
function bagRecord(
|
||||||
|
sourceType: SourceType,
|
||||||
|
bag: {
|
||||||
|
title?: string | null;
|
||||||
|
buyerName?: string | null;
|
||||||
|
procedureType?: string | null;
|
||||||
|
legalFramework?: string | null;
|
||||||
|
deadlineAt?: string | null;
|
||||||
|
},
|
||||||
|
): RawTenderRecord {
|
||||||
|
const sourcePortal =
|
||||||
|
sourceType === 'cosinex-dtvp' ? 'cosinex-dtvp' : 'tender24';
|
||||||
|
return {
|
||||||
|
sourceType,
|
||||||
|
sourcePortal,
|
||||||
|
sourceNoticeId: 'bag-notice-1',
|
||||||
|
ocid: undefined,
|
||||||
|
sourceUrl: `https://example.invalid/${sourcePortal}`,
|
||||||
|
fetchedAt: new Date(),
|
||||||
|
publishedAt: null,
|
||||||
|
eformsPayload: null,
|
||||||
|
ocdsPayload: bag,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe('TenderNormalizerService', () => {
|
describe('TenderNormalizerService', () => {
|
||||||
const service = new TenderNormalizerService();
|
const service = new TenderNormalizerService();
|
||||||
|
|
||||||
@@ -210,4 +240,81 @@ describe('TenderNormalizerService', () => {
|
|||||||
|
|
||||||
expect(mutated.contentHash).not.toBe(original.contentHash);
|
expect(mutated.contentHash).not.toBe(original.contentHash);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('generic ocdsPayload bag (ai-netserver / cosinex-dtvp gap closure)', () => {
|
||||||
|
const fullBag = {
|
||||||
|
title: 'Bauleistung X',
|
||||||
|
buyerName: 'Stadt Y',
|
||||||
|
procedureType: 'Offenes Verfahren',
|
||||||
|
legalFramework: 'VOB/A',
|
||||||
|
deadlineAt: '2026-09-01T10:00:00.000Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('ai-netserver: maps the flat bag into NormalizedTenderFields', () => {
|
||||||
|
const raw = bagRecord('ai-netserver', fullBag);
|
||||||
|
|
||||||
|
const normalized = service.normalize(raw);
|
||||||
|
|
||||||
|
expect(normalized.title).toBe('Bauleistung X');
|
||||||
|
expect(normalized.buyerName).toBe('Stadt Y');
|
||||||
|
expect(normalized.procedureType).toBe('Offenes Verfahren');
|
||||||
|
expect(normalized.deadlineAt).toBeInstanceOf(Date);
|
||||||
|
expect(normalized.deadlineAt?.toISOString()).toBe(
|
||||||
|
'2026-09-01T10:00:00.000Z',
|
||||||
|
);
|
||||||
|
expect(normalized.cpvCodes).toEqual([]);
|
||||||
|
expect(normalized.cpvDivisions).toEqual([]);
|
||||||
|
expect(normalized.region).toBeNull();
|
||||||
|
expect(normalized.plz).toBeNull();
|
||||||
|
expect(normalized.bundesland).toBeNull();
|
||||||
|
expect(normalized.estimatedValue).toBeNull();
|
||||||
|
expect(normalized.dedupKey).toBe(`${raw.sourcePortal}:${raw.sourceNoticeId}`);
|
||||||
|
expect(normalized.status).toBe('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cosinex-dtvp: maps an identical bag shape identically', () => {
|
||||||
|
const raw = bagRecord('cosinex-dtvp', fullBag);
|
||||||
|
|
||||||
|
const normalized = service.normalize(raw);
|
||||||
|
|
||||||
|
expect(normalized.title).toBe('Bauleistung X');
|
||||||
|
expect(normalized.buyerName).toBe('Stadt Y');
|
||||||
|
expect(normalized.procedureType).toBe('Offenes Verfahren');
|
||||||
|
expect(normalized.deadlineAt?.toISOString()).toBe(
|
||||||
|
'2026-09-01T10:00:00.000Z',
|
||||||
|
);
|
||||||
|
expect(normalized.cpvCodes).toEqual([]);
|
||||||
|
expect(normalized.cpvDivisions).toEqual([]);
|
||||||
|
expect(normalized.region).toBeNull();
|
||||||
|
expect(normalized.plz).toBeNull();
|
||||||
|
expect(normalized.bundesland).toBeNull();
|
||||||
|
expect(normalized.estimatedValue).toBeNull();
|
||||||
|
expect(normalized.dedupKey).toBe(`${raw.sourcePortal}:${raw.sourceNoticeId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('null/empty bag fields: buyerName/procedureType/deadlineAt stay null, title falls back', () => {
|
||||||
|
const raw = bagRecord('ai-netserver', {
|
||||||
|
title: '',
|
||||||
|
buyerName: null,
|
||||||
|
procedureType: null,
|
||||||
|
legalFramework: null,
|
||||||
|
deadlineAt: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalized = service.normalize(raw);
|
||||||
|
|
||||||
|
expect(normalized.title).toBe('Unbenannte Ausschreibung');
|
||||||
|
expect(normalized.buyerName).toBeNull();
|
||||||
|
expect(normalized.procedureType).toBeNull();
|
||||||
|
expect(normalized.deadlineAt).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contentHash for a bag source is a stable sha256 (proves shared assemble() reuse)', () => {
|
||||||
|
const raw = bagRecord('ai-netserver', fullBag);
|
||||||
|
|
||||||
|
const normalized = service.normalize(raw);
|
||||||
|
|
||||||
|
expect(normalized.contentHash).toMatch(/^[a-f0-9]{64}$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user