UNPKG

@accounter/shaam-uniform-format-generator

Version:

Fully typed application that generates, parses, and validates SHAAM uniform format tax reports (INI.TXT and BKMVDATA.TXT).

240 lines (195 loc) • 8.38 kB
import { describe, expect, it } from 'vitest'; import { A100Schema, encodeA100, parseA100, type A100 } from '../../src/generator/records/a100.js'; describe('A100 Record', () => { const validA100: A100 = { code: 'A100', recordNumber: 1, vatId: '123456789', primaryIdentifier: Number('12345678901234'), // This is a safe integer in JS systemConstant: '&OF1.31&', reserved: '', }; describe('A100Schema validation', () => { it('should validate a complete A100 record', () => { expect(() => A100Schema.parse(validA100)).not.toThrow(); }); it('should require code to be exactly "A100"', () => { expect(() => A100Schema.parse({ ...validA100, code: 'A10' })).toThrow(); expect(() => A100Schema.parse({ ...validA100, code: 'A1000' })).toThrow(); expect(() => A100Schema.parse({ ...validA100, code: 'B100' })).toThrow(); }); it('should require non-empty recordNumber', () => { expect(() => A100Schema.parse({ ...validA100, recordNumber: '' })).toThrow(); }); it('should require non-empty vatId', () => { expect(() => A100Schema.parse({ ...validA100, vatId: '' })).toThrow(); }); it('should require non-empty primaryIdentifier', () => { expect(() => A100Schema.parse({ ...validA100, primaryIdentifier: '' })).toThrow(); }); it('should allow empty reserved field', () => { const valid = { ...validA100, reserved: '' }; expect(() => A100Schema.parse(valid)).not.toThrow(); }); it('should enforce maximum field lengths', () => { expect(() => A100Schema.parse({ ...validA100, recordNumber: '1234567890' })).toThrow(); expect(() => A100Schema.parse({ ...validA100, vatId: '1234567890' })).toThrow(); expect(() => A100Schema.parse({ ...validA100, primaryIdentifier: '1234567890123456' }), ).toThrow(); expect(() => A100Schema.parse({ ...validA100, reserved: 'x'.repeat(51) })).toThrow(); }); }); describe('encodeA100', () => { it('should encode a valid A100 record to fixed-width format', () => { const encoded = encodeA100(validA100); // Should end with CRLF expect(encoded).toMatch(/\r\n$/); // Remove CRLF for length check const withoutCrlf = encoded.replace(/\r\n$/, ''); expect(withoutCrlf).toHaveLength(95); // Check specific field positions expect(withoutCrlf.slice(0, 4)).toBe('A100'); // Record code expect(withoutCrlf.slice(4, 13)).toBe('000000001'); // Zero-padded record number (numeric field) expect(withoutCrlf.slice(13, 22)).toBe('123456789'); // Zero-padded VAT ID (already 9 digits) expect(withoutCrlf.slice(22, 37)).toBe('012345678901234'); // Zero-padded primary identifier (numeric field) expect(withoutCrlf.slice(37, 45)).toBe('&OF1.31&'); // System constant }); it('should handle long fields by truncating', () => { const longFields: A100 = { ...validA100, vatId: '1234567890', // Too long for 9 chars primaryIdentifier: Number('123456789012345'), // 15 digits - max for this field }; const encoded = encodeA100(longFields); const withoutCrlf = encoded.replace(/\r\n$/, ''); // Should still be exactly 95 characters expect(withoutCrlf).toHaveLength(95); // VAT ID should be truncated to 9 chars and zero-padded expect(withoutCrlf.slice(13, 22)).toBe('123456789'); // Primary identifier should be truncated to 15 chars and zero-padded expect(withoutCrlf.slice(22, 37)).toBe('123456789012345'); // System constant should always be the constant expect(withoutCrlf.slice(37, 45)).toBe('&OF1.31&'); }); it('should pad short fields correctly', () => { const shortFields: A100 = { ...validA100, recordNumber: 42, vatId: '123', primaryIdentifier: 456, }; const encoded = encodeA100(shortFields); const withoutCrlf = encoded.replace(/\r\n$/, ''); // Record number should be zero-padded (numeric field) expect(withoutCrlf.slice(4, 13)).toBe('000000042'); // VAT ID should be zero-padded (numeric field) expect(withoutCrlf.slice(13, 22)).toBe('000000123'); // Primary identifier should be zero-padded (numeric field) expect(withoutCrlf.slice(22, 37)).toBe('000000000000456'); // System constant should always be the constant expect(withoutCrlf.slice(37, 45)).toBe('&OF1.31&'); }); }); describe('parseA100', () => { it('should parse a valid encoded A100 record', () => { const encoded = encodeA100(validA100); const parsed = parseA100(encoded); expect(parsed.code).toBe('A100'); expect(parsed.recordNumber).toBe(1); expect(parsed.vatId).toBe('123456789'); expect(parsed.primaryIdentifier).toBe(Number('12345678901234')); // This is a safe integer in JS expect(parsed.systemConstant).toBe('&OF1.31&'); expect(parsed.reserved).toBe(''); }); it('should handle lines without CRLF', () => { const encoded = encodeA100(validA100); const withoutCrlf = encoded.replace(/\r\n$/, ''); const parsed = parseA100(withoutCrlf); expect(parsed).toEqual(validA100); }); it('should throw on invalid line length', () => { const shortLine = 'A100 1123456789'; expect(() => parseA100(shortLine)).toThrow('Invalid A100 record length'); }); it('should throw on invalid record code', () => { const invalidCode = 'B100' + ' '.repeat(91); expect(() => parseA100(invalidCode)).toThrow('Invalid A100 record code'); }); it('should throw on invalid system constant', () => { const invalidVersion = 'A100' + // code (4) ' 1' + // recordNumber (9) '123456789' + // vatId (9) '12345678901234 ' + // primaryIdentifier (15) 'INVALID ' + // invalid systemConstant (8) ' '.repeat(50); // reserved (50) expect(() => parseA100(invalidVersion)).toThrow('Invalid system constant'); }); it('should trim whitespace from parsed fields', () => { const paddedLine = 'A100' + // code (4) '000000042' + // recordNumber (9) - zero-padded '000000123' + // vatId (9) - zero-padded '000000000000123' + // primaryIdentifier (15) - zero-padded '&OF1.31&' + // systemConstant (8) ' '.repeat(50); // reserved (50) expect(paddedLine.length).toBe(95); // Verify correct length const parsed = parseA100(paddedLine); expect(parsed.recordNumber).toBe(42); expect(parsed.vatId).toBe('123'); expect(parsed.primaryIdentifier).toBe(123); expect(parsed.reserved).toBe(''); }); }); describe('round-trip encoding/parsing', () => { it('should maintain data integrity through encode/parse cycle', () => { const original = validA100; const encoded = encodeA100(original); const parsed = parseA100(encoded); expect(parsed).toEqual(original); }); it('should work with various field values', () => { const testCases: A100[] = [ { code: 'A100', recordNumber: Number('999999999'), vatId: '999888777', primaryIdentifier: Number('123456789012345'), systemConstant: '&OF1.31&', reserved: 'Some reserved content', }, { code: 'A100', recordNumber: 1, vatId: '1', primaryIdentifier: 1, systemConstant: '&OF1.31&', reserved: '', }, ]; for (const testCase of testCases) { const encoded = encodeA100(testCase); const parsed = parseA100(encoded); expect(parsed).toEqual(testCase); } }); it('should preserve empty reserved field', () => { const withEmptyReserved: A100 = { ...validA100, reserved: '', }; const encoded = encodeA100(withEmptyReserved); const parsed = parseA100(encoded); expect(parsed.reserved).toBe(''); expect(parsed).toEqual(withEmptyReserved); }); }); describe('error cases', () => { it('should handle malformed input gracefully', () => { expect(() => parseA100('')).toThrow(); expect(() => parseA100('invalid')).toThrow(); expect(() => parseA100('A' + ' '.repeat(100))).toThrow(); }); }); });