/** * Unit tests for the importer engine internals (no DB): fuzzy column mapping, * row validation (per-field + cross-field), natural-key derivation, and CSV * parsing. */ import { describe, expect, it } from 'vitest'; import { applyMapping, suggestMapping } from '@/lib/import/mapping'; import { validateRow } from '@/lib/import/classify'; import { parseCsv } from '@/lib/import/engine'; import { companiesAdapter } from '@/lib/import/adapters/companies'; import { clientsAdapter } from '@/lib/import/adapters/clients'; import { berthsAdapter } from '@/lib/import/adapters/berths'; describe('suggestMapping', () => { it('maps headers to fields by exact, alias, and fuzzy match', () => { const m = suggestMapping( ['Company Name', 'VAT', 'Billing Email', 'Unrelated'], companiesAdapter.targetFields, ); expect(m.name).toBe('Company Name'); // alias "companyname" expect(m.taxId).toBe('VAT'); // alias "vat" expect(m.billingEmail).toBe('Billing Email'); // A header claimed by one field isn't reused by another. expect(Object.values(m).filter((h) => h === 'Company Name')).toHaveLength(1); }); it('leaves unmatched fields out of the mapping', () => { const m = suggestMapping(['xyz', 'qqq'], companiesAdapter.targetFields); expect(m.name).toBeUndefined(); }); }); describe('applyMapping', () => { it('produces fieldKey→cell and drops empty cells', () => { const mapped = applyMapping( { 'Company Name': ' Acme ', VAT: '', Email: 'a@b.com' }, { name: 'Company Name', taxId: 'VAT', billingEmail: 'Email' }, ); expect(mapped).toEqual({ name: 'Acme', billingEmail: 'a@b.com' }); }); }); describe('validateRow', () => { it('flags a missing required field', () => { const errs = validateRow(companiesAdapter, { legalName: 'X Ltd' }); expect(errs).toEqual([{ field: 'name', message: 'Name is required' }]); }); it('flags an invalid email', () => { const errs = validateRow(companiesAdapter, { name: 'Acme', billingEmail: 'not-an-email' }); expect(errs.some((e) => e.field === 'billingEmail')).toBe(true); }); it('clients require an email or a phone (cross-field)', () => { expect(validateRow(clientsAdapter, { fullName: 'Jane Doe' })).toEqual([ { field: 'email', message: 'An email or phone is required' }, ]); expect(validateRow(clientsAdapter, { fullName: 'Jane Doe', email: 'j@d.com' })).toEqual([]); }); it('berths reject a malformed mooring number', () => { const errs = validateRow(berthsAdapter, { mooringNumber: 'not-a-mooring', area: 'A' }); expect(errs.some((e) => e.field === 'mooringNumber')).toBe(true); }); }); describe('matchKey', () => { it('companies: case-insensitive name', () => { expect(companiesAdapter.matchKey({ name: ' AcMe ' })).toBe('acme'); }); it('berths: canonicalizes mooring (D-032 → D32)', () => { expect(berthsAdapter.matchKey({ mooringNumber: 'd-032', area: 'D' })).toBe('D32'); }); it('clients: email key, phone fallback', () => { expect(clientsAdapter.matchKey({ fullName: 'X', email: 'A@B.com' })).toBe('email:a@b.com'); const k = clientsAdapter.matchKey({ fullName: 'X', phone: '+1 574 274 0548' }); expect(k?.startsWith('phone:')).toBe(true); }); }); describe('parseCsv', () => { it('parses headers + rows, trims, skips blank lines', () => { const { headers, rows } = parseCsv('Name, Email\nAcme, a@b.com\n\nBeta, b@c.com\n'); expect(headers).toEqual(['Name', 'Email']); expect(rows).toEqual([ { Name: 'Acme', Email: 'a@b.com' }, { Name: 'Beta', Email: 'b@c.com' }, ]); }); });