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).

420 lines (376 loc) • 15.1 kB
import { describe, expect, it } from 'vitest'; import { B100Schema, encodeB100, parseB100, type B100 } from '../../src/generator/records/b100'; describe('B100 Record', () => { const validB100: B100 = { code: 'B100', recordNumber: '1', vatId: '123456789', transactionNumber: '1234567890', transactionLineNumber: '1', batchNumber: '12345678', transactionType: 'Sale', referenceDocument: 'INV-2023-001', referenceDocumentType: '1', referenceDocument2: 'REF-2023-001', referenceDocumentType2: '2', details: 'Payment for services', date: '20231215', valueDate: '20231215', accountKey: 'ACC001', counterAccountKey: 'ACC002', debitCreditIndicator: '1', currencyCode: 'ILS', transactionAmount: '1000.00', foreignCurrencyAmount: '300.00', quantityField: '5.00', matchingField1: 'MATCH001', matchingField2: 'MATCH002', branchId: 'BR001', entryDate: '20231215', operatorUsername: 'operator1', reserved: '', }; describe('B100Schema validation', () => { it('should validate a complete B100 record', () => { expect(() => B100Schema.parse(validB100)).not.toThrow(); }); it('should require code to be exactly "B100"', () => { expect(() => B100Schema.parse({ ...validB100, code: 'B10' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, code: 'B1000' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, code: 'A100' })).toThrow(); }); it('should require non-empty required fields', () => { expect(() => B100Schema.parse({ ...validB100, recordNumber: '' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, vatId: '' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, transactionNumber: '' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, transactionLineNumber: '' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, accountKey: '' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, transactionAmount: '' })).toThrow(); }); it('should allow empty optional fields', () => { const minimal: B100 = { code: 'B100', recordNumber: '1', vatId: '123456789', transactionNumber: '1234567890', transactionLineNumber: '1', batchNumber: '', transactionType: '', referenceDocument: '', referenceDocumentType: '', referenceDocument2: '', referenceDocumentType2: '', details: '', date: '20231215', valueDate: '20231215', accountKey: 'ACC001', counterAccountKey: '', debitCreditIndicator: '1', currencyCode: '', transactionAmount: '1000.00', foreignCurrencyAmount: '', quantityField: '', matchingField1: '', matchingField2: '', branchId: '', entryDate: '20231215', operatorUsername: '', reserved: '', }; expect(() => B100Schema.parse(minimal)).not.toThrow(); }); it('should validate debit/credit indicator', () => { expect(() => B100Schema.parse({ ...validB100, debitCreditIndicator: '1' })).not.toThrow(); expect(() => B100Schema.parse({ ...validB100, debitCreditIndicator: '2' })).not.toThrow(); expect(() => B100Schema.parse({ ...validB100, debitCreditIndicator: '0' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, debitCreditIndicator: '3' })).toThrow(); }); it('should validate date format (8 digits)', () => { expect(() => B100Schema.parse({ ...validB100, date: '2023121' })).toThrow(); // Too short expect(() => B100Schema.parse({ ...validB100, date: '202312150' })).toThrow(); // Too long expect(() => B100Schema.parse({ ...validB100, date: '20231a15' })).toThrow(); // Non-numeric }); it('should enforce maximum field lengths', () => { expect(() => B100Schema.parse({ ...validB100, recordNumber: '1234567890' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, vatId: '1234567890' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, transactionNumber: '12345678901' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, transactionLineNumber: '123456' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, batchNumber: '123456789' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, transactionType: 'x'.repeat(16) })).toThrow(); expect(() => B100Schema.parse({ ...validB100, referenceDocument: 'x'.repeat(21) })).toThrow(); expect(() => B100Schema.parse({ ...validB100, details: 'x'.repeat(51) })).toThrow(); expect(() => B100Schema.parse({ ...validB100, accountKey: 'x'.repeat(16) })).toThrow(); expect(() => B100Schema.parse({ ...validB100, reserved: 'x'.repeat(26) })).toThrow(); }); }); describe('encodeB100', () => { it('should encode a valid B100 record to fixed-width format', () => { const encoded = encodeB100(validB100); // Should end with CRLF expect(encoded).toMatch(/\r\n$/); // Remove CRLF for length check const withoutCrlf = encoded.replace(/\r\n$/, ''); expect(withoutCrlf).toHaveLength(317); // Check specific field positions expect(withoutCrlf.slice(0, 4)).toBe('B100'); // Record code expect(withoutCrlf.slice(4, 13)).toBe('000000001'); // Zero-padded record number expect(withoutCrlf.slice(13, 22)).toBe('123456789'); // VAT ID expect(withoutCrlf.slice(22, 32)).toBe('1234567890'); // Transaction number expect(withoutCrlf.slice(32, 37)).toBe('00001'); // Zero-padded transaction line number }); it('should handle numeric fields with zero padding', () => { const shortFields: B100 = { ...validB100, recordNumber: '42', transactionNumber: '123', transactionLineNumber: '5', batchNumber: '99', }; const encoded = encodeB100(shortFields); const withoutCrlf = encoded.replace(/\r\n$/, ''); // Record number should be zero-padded expect(withoutCrlf.slice(4, 13)).toBe('000000042'); // Transaction number should be zero-padded expect(withoutCrlf.slice(22, 32)).toBe('0000000123'); // Transaction line number should be zero-padded expect(withoutCrlf.slice(32, 37)).toBe('00005'); // Batch number should be zero-padded expect(withoutCrlf.slice(37, 45)).toBe('00000099'); }); it('should handle amount fields with right alignment', () => { const encoded = encodeB100(validB100); const withoutCrlf = encoded.replace(/\r\n$/, ''); // Find transaction amount field position (after all previous fields) const transactionAmountPos = 4 + 9 + 9 + 10 + 5 + 8 + 15 + 20 + 3 + 20 + 3 + 50 + 8 + 8 + 15 + 15 + 1 + 3; expect(withoutCrlf.slice(transactionAmountPos, transactionAmountPos + 15)).toBe( ' 1000.00', ); // Right-aligned }); it('should pad empty fields correctly', () => { const minimal: B100 = { code: 'B100', recordNumber: '1', vatId: '123456789', transactionNumber: '1', transactionLineNumber: '1', batchNumber: '', transactionType: '', referenceDocument: '', referenceDocumentType: '', referenceDocument2: '', referenceDocumentType2: '', details: '', date: '20231215', valueDate: '20231215', accountKey: 'ACC001', counterAccountKey: '', debitCreditIndicator: '1', currencyCode: '', transactionAmount: '1000.00', foreignCurrencyAmount: '', quantityField: '', matchingField1: '', matchingField2: '', branchId: '', entryDate: '20231215', operatorUsername: '', reserved: '', }; const encoded = encodeB100(minimal); const withoutCrlf = encoded.replace(/\r\n$/, ''); // Should still be exactly 317 characters expect(withoutCrlf).toHaveLength(317); // Empty batch number should be zero-padded expect(withoutCrlf.slice(37, 45)).toBe('00000000'); }); }); describe('parseB100', () => { it('should parse a valid encoded B100 record', () => { const encoded = encodeB100(validB100); const parsed = parseB100(encoded); expect(parsed.code).toBe('B100'); expect(parsed.recordNumber).toBe('1'); expect(parsed.vatId).toBe('123456789'); expect(parsed.transactionNumber).toBe('1234567890'); expect(parsed.transactionLineNumber).toBe('1'); expect(parsed.accountKey).toBe('ACC001'); expect(parsed.debitCreditIndicator).toBe('1'); expect(parsed.date).toBe('20231215'); expect(parsed.transactionAmount).toBe('1000.00'); }); it('should handle lines without CRLF', () => { const encoded = encodeB100(validB100); const withoutCrlf = encoded.replace(/\r\n$/, ''); const parsed = parseB100(withoutCrlf); expect(parsed.code).toBe(validB100.code); expect(parsed.recordNumber).toBe(validB100.recordNumber); }); it('should throw on invalid line length', () => { const shortLine = 'B100 1123456789'; expect(() => parseB100(shortLine)).toThrow('Invalid B100 record length'); }); it('should throw on invalid record code', () => { const invalidCode = 'A100' + ' '.repeat(313); expect(() => parseB100(invalidCode)).toThrow('Invalid B100 record code'); }); it('should strip leading zeros from numeric fields correctly', () => { // Create a line with zero-padded numeric fields const paddedLine = 'B100' + // code (4) '000000042' + // recordNumber (9) '000000123' + // vatId (9) '0000000456' + // transactionNumber (10) '00005' + // transactionLineNumber (5) '00000099' + // batchNumber (8) ' '.repeat(15) + // transactionType (15) ' '.repeat(20) + // referenceDocument (20) '000' + // referenceDocumentType (3) ' '.repeat(20) + // referenceDocument2 (20) '000' + // referenceDocumentType2 (3) ' '.repeat(50) + // details (50) '20231215' + // date (8) '20231215' + // valueDate (8) 'ACC001 ' + // accountKey (15) ' '.repeat(15) + // counterAccountKey (15) '1' + // debitCreditIndicator (1) ' ' + // currencyCode (3) ' 1000.00' + // transactionAmount (15) ' '.repeat(15) + // foreignCurrencyAmount (15) ' '.repeat(12) + // quantityField (12) ' '.repeat(10) + // matchingField1 (10) ' '.repeat(10) + // matchingField2 (10) ' '.repeat(7) + // branchId (7) '20231215' + // entryDate (8) ' '.repeat(9) + // operatorUsername (9) ' '.repeat(25); // reserved (25) expect(paddedLine.length).toBe(317); // Verify correct length const parsed = parseB100(paddedLine); expect(parsed.recordNumber).toBe('42'); expect(parsed.vatId).toBe('123'); expect(parsed.transactionNumber).toBe('456'); expect(parsed.transactionLineNumber).toBe('5'); expect(parsed.batchNumber).toBe('99'); expect(parsed.referenceDocumentType).toBe(''); expect(parsed.referenceDocumentType2).toBe(''); }); it('should trim whitespace from parsed fields', () => { const encoded = encodeB100(validB100); const parsed = parseB100(encoded); // All string fields should be trimmed expect(parsed.transactionType).toBe('Sale'); expect(parsed.referenceDocument).toBe('INV-2023-001'); expect(parsed.details).toBe('Payment for services'); expect(parsed.accountKey).toBe('ACC001'); }); }); describe('round-trip encoding/parsing', () => { it('should maintain data integrity through encode/parse cycle', () => { const original = validB100; const encoded = encodeB100(original); const parsed = parseB100(encoded); expect(parsed).toEqual(original); }); it('should work with various field values', () => { const testCases: B100[] = [ { ...validB100, recordNumber: '999999999', transactionNumber: '9999999999', transactionLineNumber: '99999', debitCreditIndicator: '2', }, { ...validB100, recordNumber: '1', transactionNumber: '1', transactionLineNumber: '1', batchNumber: '', transactionType: '', referenceDocument: '', details: '', counterAccountKey: '', currencyCode: '', foreignCurrencyAmount: '', quantityField: '', matchingField1: '', matchingField2: '', branchId: '', operatorUsername: '', reserved: '', }, ]; for (const testCase of testCases) { const encoded = encodeB100(testCase); const parsed = parseB100(encoded); expect(parsed).toEqual(testCase); } }); it('should preserve empty optional fields', () => { const withEmptyFields: B100 = { ...validB100, batchNumber: '', transactionType: '', referenceDocument: '', referenceDocumentType: '', referenceDocument2: '', referenceDocumentType2: '', details: '', counterAccountKey: '', currencyCode: '', foreignCurrencyAmount: '', quantityField: '', matchingField1: '', matchingField2: '', branchId: '', operatorUsername: '', reserved: '', }; const encoded = encodeB100(withEmptyFields); const parsed = parseB100(encoded); expect(parsed.batchNumber).toBe(''); expect(parsed.transactionType).toBe(''); expect(parsed.reserved).toBe(''); expect(parsed).toEqual(withEmptyFields); }); }); describe('error cases', () => { it('should handle malformed input gracefully', () => { expect(() => parseB100('')).toThrow(); expect(() => parseB100('invalid')).toThrow(); expect(() => parseB100('B' + ' '.repeat(400))).toThrow(); }); it('should validate against schema after parsing', () => { // Create a line with invalid debit/credit indicator const invalidLine = 'B100' + '000000001' + '123456789' + '1234567890' + '00001' + '00000000' + ' '.repeat(15) + ' '.repeat(20) + '000' + ' '.repeat(20) + '000' + ' '.repeat(50) + '20231215' + '20231215' + 'ACC001 ' + ' '.repeat(15) + '9' + // Invalid debit/credit indicator ' '.repeat(3) + ' 1000.00' + ' '.repeat(15) + ' '.repeat(12) + ' '.repeat(10) + ' '.repeat(10) + ' '.repeat(7) + '20231215' + ' '.repeat(9) + ' '.repeat(25); expect(() => parseB100(invalidLine)).toThrow(); }); }); });