import { describe, expect, it } from 'vitest'; import { tenderFingerprint } from './tender-fingerprint'; /** * SCHEMA-03 (D-04) — pure fingerprint function. NULL-tolerant because the * live DB is 92.4% NULL estimatedValue / 15.7% NULL deadlineAt (13-RESEARCH * Pattern 4) — a fingerprint that requires those fields would never match * the overwhelming majority of real DÖE tenders. */ describe('tenderFingerprint', () => { const base = { buyerName: 'Stadt München', title: 'Neubau einer Kindertagesstätte', cpvDivisions: ['45', '71'], deadlineAt: null as Date | null, estimatedValue: null as number | null, }; it('matches when title+buyer+cpv are equal and value+deadline are both NULL', () => { const a = tenderFingerprint({ ...base }); const b = tenderFingerprint({ ...base }); expect(a).toBe(b); }); it('differs for a different title (collision guard, Pitfall 1)', () => { const a = tenderFingerprint(base); const b = tenderFingerprint({ ...base, title: 'Sanierung eines Kindergartens' }); expect(a).not.toBe(b); }); it('normalizes umlauts so "München" and "Muenchen" contribute the same text', () => { const a = tenderFingerprint({ ...base, buyerName: 'Stadt München' }); const b = tenderFingerprint({ ...base, buyerName: 'Stadt Muenchen' }); expect(a).toBe(b); }); it('is order-independent and deduplicated for cpvDivisions', () => { const a = tenderFingerprint({ ...base, cpvDivisions: ['45', '71'] }); const b = tenderFingerprint({ ...base, cpvDivisions: ['71', '45', '71'] }); expect(a).toBe(b); }); it('buckets estimatedValue by order of magnitude — 12000 and 15000 collide', () => { const a = tenderFingerprint({ ...base, estimatedValue: 12000 }); const b = tenderFingerprint({ ...base, estimatedValue: 15000 }); expect(a).toBe(b); }); it('separates estimatedValue across a magnitude boundary', () => { const a = tenderFingerprint({ ...base, estimatedValue: 9000 }); const b = tenderFingerprint({ ...base, estimatedValue: 90000 }); expect(a).not.toBe(b); }); it('returns a stable sha256 hex string (deterministic across runs)', () => { const result = tenderFingerprint(base); expect(result).toMatch(/^[a-f0-9]{64}$/); }); it('treats a null buyerName as an empty segment, not a crash', () => { expect(() => tenderFingerprint({ ...base, buyerName: null })).not.toThrow(); const a = tenderFingerprint({ ...base, buyerName: null }); const b = tenderFingerprint({ ...base, buyerName: null }); expect(a).toBe(b); }); });