From a64f98463c38176421897454cdbbb963ed4eb15c Mon Sep 17 00:00:00 2001 From: Schalli Date: Tue, 21 Jul 2026 16:08:07 +0200 Subject: [PATCH] test(11-03): add failing spec for CPV-Divisions-Katalog + normalizeCpv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RED-first (TDD): normalizeCpv/divisionOf/cpvMatchesDivisions across the three inconsistent live-DB CPV formats ("45", "45000000", "45000000-7") plus a CPV_DIVISIONS catalog shape check — module does not exist yet. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/src/tenders/cpv/cpv-catalog.spec.ts | 104 +++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 apps/api/src/tenders/cpv/cpv-catalog.spec.ts diff --git a/apps/api/src/tenders/cpv/cpv-catalog.spec.ts b/apps/api/src/tenders/cpv/cpv-catalog.spec.ts new file mode 100644 index 0000000..13e00e0 --- /dev/null +++ b/apps/api/src/tenders/cpv/cpv-catalog.spec.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from 'vitest'; +import { + CPV_DIVISIONS, + cpvMatchesDivisions, + divisionOf, + normalizeCpv, +} from './cpv-catalog'; + +/** + * cpv-catalog.spec — RED-first (TDD) proof for FILTER-03 (D-03, Pitfall 2). + * + * Core test: cpvCodes arrive in the live DB in mixed formats ("45", + * "45000000", "45000000-7") — the normalizer/matcher must handle all three + * via two-sided normalization down to the leading 2-digit CPV division, + * never an exact-equality match against a single canonical shape. + */ + +describe('normalizeCpv', () => { + it('strips the check-digit suffix: "45000000-7" -> "45000000"', () => { + expect(normalizeCpv('45000000-7')).toBe('45000000'); + }); + + it('passes through an already-8-digit code unchanged', () => { + expect(normalizeCpv('45000000')).toBe('45000000'); + }); + + it('passes through a bare 2-digit division code unchanged', () => { + expect(normalizeCpv('45')).toBe('45'); + }); + + it('strips non-digit characters generally, not only a trailing dash-digit', () => { + expect(normalizeCpv('45000000-7 ')).toBe('45000000'); + }); + + it('returns an empty string for null/empty input, never throws', () => { + expect(normalizeCpv('')).toBe(''); + expect(normalizeCpv(null as unknown as string)).toBe(''); + expect(normalizeCpv(undefined as unknown as string)).toBe(''); + }); +}); + +describe('divisionOf', () => { + it('extracts the leading 2 digits from an 8-digit code', () => { + expect(divisionOf('45000000')).toBe('45'); + }); + + it('is a no-op for an already-2-digit division code', () => { + expect(divisionOf('45')).toBe('45'); + }); + + it('extracts the leading 2 digits from a raw code with a check-digit suffix', () => { + expect(divisionOf('45000000-7')).toBe('45'); + }); + + it('returns an empty string for empty/invalid input, never throws', () => { + expect(divisionOf('')).toBe(''); + expect(divisionOf('-')).toBe(''); + }); +}); + +describe('cpvMatchesDivisions', () => { + it('matches when any code in the array normalizes into a selected division', () => { + expect( + cpvMatchesDivisions(['45000000-7', '71000000'], ['45']), + ).toBe(true); + }); + + it('matches a bare 2-digit division code against itself', () => { + expect(cpvMatchesDivisions(['45'], ['45'])).toBe(true); + }); + + it('returns false when no code matches any selected division', () => { + expect(cpvMatchesDivisions(['71000000', '90500000'], ['45'])).toBe(false); + }); + + it('returns false for an empty codes array, never throws', () => { + expect(cpvMatchesDivisions([], ['45'])).toBe(false); + }); + + it('returns false when no divisions are selected', () => { + expect(cpvMatchesDivisions(['45000000'], [])).toBe(false); + }); +}); + +describe('CPV_DIVISIONS catalog', () => { + it('contains the Bauarbeiten division (45) with a German label', () => { + const entry = CPV_DIVISIONS.find((d) => d.code === '45'); + expect(entry).toBeDefined(); + expect(entry?.label).toBe('Bauarbeiten'); + }); + + it('every entry has a 2-digit numeric code and a non-empty label', () => { + expect(CPV_DIVISIONS.length).toBeGreaterThan(30); + for (const entry of CPV_DIVISIONS) { + expect(entry.code).toMatch(/^\d{2}$/); + expect(entry.label.length).toBeGreaterThan(0); + } + }); + + it('has no duplicate codes', () => { + const codes = CPV_DIVISIONS.map((d) => d.code); + expect(new Set(codes).size).toBe(codes.length); + }); +});