feat(11-03): cpvDivisions column — normalizer + backfill migration
Adds Tender.cpvDivisions String[] (@@index Gin) derived at ingestion time via cpv-catalog.ts's divisionOf() — replaces exact-match cpvCodes comparison with a typesafe, GIN-indexable hasSome target (FILTER-03, Pitfall 2). Backfill migration 20260721150000_tender_cpv_divisions_backfill applied locally: 1612/1671 rows populated across all observed divisions (741 rows carry division '45' — Bauarbeiten); idempotent (second run: UPDATE 0, ADD COLUMN IF NOT EXISTS / CREATE INDEX IF NOT EXISTS both skip cleanly). Applied via docker exec psql + `prisma migrate resolve --applied` + `prisma generate` against the local dev DB only — no Docker deploy on the test server. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,42 @@ describe('TenderNormalizerService', () => {
|
||||
expect(normalized.bundesland).toBeNull();
|
||||
});
|
||||
|
||||
it('derives distinct cpvDivisions (leading 2 digits) from cpvCodes (FILTER-03, Pitfall 2)', () => {
|
||||
const raw = loadRawNoticePair(NOTICE_WITH_EFORMS_ONLY_DEADLINE);
|
||||
|
||||
const normalized = service.normalize(raw);
|
||||
|
||||
expect(Array.isArray(normalized.cpvDivisions)).toBe(true);
|
||||
for (const division of normalized.cpvDivisions) {
|
||||
expect(division).toMatch(/^\d{2}$/);
|
||||
}
|
||||
// Every cpvCodes entry must normalize into one of the derived divisions.
|
||||
for (const code of normalized.cpvCodes) {
|
||||
const digits = code.replace(/\D/g, '');
|
||||
if (digits.length >= 2) {
|
||||
expect(normalized.cpvDivisions).toContain(digits.slice(0, 2));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('cpvDivisions is an empty array when cpvCodes is empty — never crashes', () => {
|
||||
const raw = loadRawNoticePair(NOTICE_WITH_NO_DEADLINE_OR_VALUE);
|
||||
const rawWithoutCpv: RawTenderRecord = {
|
||||
...raw,
|
||||
ocdsPayload: {
|
||||
...(raw.ocdsPayload as Record<string, unknown>),
|
||||
tender: {
|
||||
...(raw.ocdsPayload as { tender?: Record<string, unknown> })?.tender,
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const normalized = service.normalize(rawWithoutCpv);
|
||||
|
||||
expect(normalized.cpvDivisions).toEqual([]);
|
||||
});
|
||||
|
||||
it('dedupKey = ocid when present', () => {
|
||||
const raw = loadRawNoticePair(NOTICE_WITH_EFORMS_ONLY_DEADLINE);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user