test(11-03): add failing spec for CPV-Divisions-Katalog + normalizeCpv

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) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 16:08:07 +02:00
parent a4c02116c1
commit a64f98463c
@@ -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);
});
});