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

423 lines (379 loc) • 15.3 kB
import { describe, expect, it } from 'vitest'; import { B100Schema, encodeB100, parseB100, type B100 } from '../../src/generator/records/b100.js'; describe('B100 Record', () => { const validB100: B100 = { code: 'B100', recordNumber: 1, vatId: '123456789', transactionNumber: 1_234_567_890, transactionLineNumber: 1, batchNumber: 12_345_678, transactionType: 'Sale', referenceDocument: 'INV-2023-001', referenceDocumentType: '320', referenceDocument2: 'REF-2023-001', referenceDocumentType2: '300', details: 'Payment for services', date: '20231215', valueDate: '20231215', accountKey: 'ACC001', counterAccountKey: 'ACC002', debitCreditIndicator: '1', currencyCode: 'ILS', transactionAmount: 1000.0, foreignCurrencyAmount: 300.0, quantityField: 5.0, 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: 0 })).toThrow(); expect(() => B100Schema.parse({ ...validB100, vatId: '' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, transactionNumber: 0 })).toThrow(); expect(() => B100Schema.parse({ ...validB100, transactionLineNumber: 0 })).toThrow(); expect(() => B100Schema.parse({ ...validB100, accountKey: '' })).toThrow(); // Test with invalid transactionAmount type expect(() => B100Schema.parse({ ...validB100, transactionAmount: 'invalid' as unknown as number }), ).toThrow(); }); it('should allow empty optional fields', () => { const minimal: B100 = { code: 'B100', recordNumber: 1, vatId: '123456789', transactionNumber: 1_234_567_890, transactionLineNumber: 1, batchNumber: undefined, transactionType: '', referenceDocument: '', referenceDocumentType: undefined, referenceDocument2: '', referenceDocumentType2: undefined, details: '', date: '20231215', valueDate: '20231215', accountKey: 'ACC001', counterAccountKey: '', debitCreditIndicator: '1', currencyCode: undefined, transactionAmount: 1000.0, foreignCurrencyAmount: undefined, quantityField: undefined, 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: 1_000_000_000 })).toThrow(); expect(() => B100Schema.parse({ ...validB100, vatId: '1234567890' })).toThrow(); expect(() => B100Schema.parse({ ...validB100, transactionNumber: 10_000_000_000 })).toThrow(); expect(() => B100Schema.parse({ ...validB100, transactionLineNumber: 100_000 })).toThrow(); expect(() => B100Schema.parse({ ...validB100, batchNumber: 100_000_000 })).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( '+00000000100000', ); // Monetary field with + sign and no decimal point }); it('should pad empty fields correctly', () => { const minimal: B100 = { code: 'B100', recordNumber: 1, vatId: '123456789', transactionNumber: 1, transactionLineNumber: 1, batchNumber: undefined, transactionType: '', referenceDocument: '', referenceDocumentType: undefined, referenceDocument2: '', referenceDocumentType2: undefined, details: '', date: '20231215', valueDate: '20231215', accountKey: 'ACC001', counterAccountKey: '', debitCreditIndicator: '1', currencyCode: undefined, transactionAmount: 1000.0, foreignCurrencyAmount: undefined, quantityField: undefined, 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(1_234_567_890); 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.0); }); 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) '+00000000100000' + // transactionAmount (15) - monetary field ' '.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(undefined); expect(parsed.referenceDocumentType2).toBe(undefined); }); 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: 999_999_999, transactionNumber: 9_999_999_999, transactionLineNumber: 99_999, debitCreditIndicator: '2', }, { ...validB100, recordNumber: 1, transactionNumber: 1, transactionLineNumber: 1, batchNumber: undefined, transactionType: '', referenceDocument: '', details: '', counterAccountKey: '', currencyCode: undefined, foreignCurrencyAmount: undefined, quantityField: undefined, 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: undefined, transactionType: '', referenceDocument: '', referenceDocumentType: undefined, referenceDocument2: '', referenceDocumentType2: undefined, details: '', counterAccountKey: '', currencyCode: undefined, foreignCurrencyAmount: undefined, quantityField: undefined, matchingField1: '', matchingField2: '', branchId: '', operatorUsername: '', reserved: '', }; const encoded = encodeB100(withEmptyFields); const parsed = parseB100(encoded); expect(parsed.batchNumber).toBe(undefined); 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(); }); }); });