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

442 lines (396 loc) • 17.4 kB
import { describe, expect, it } from 'vitest'; import { B110Schema, encodeB110, parseB110, type B110 } from '../../src/generator/records/b110.js'; describe('B110 Record', () => { const validB110: B110 = { code: 'B110' as const, recordNumber: 1, vatId: '123456789', accountKey: 'ACC001', accountName: 'Cash Account', trialBalanceCode: 'TB001', trialBalanceCodeDescription: 'Current Assets', customerSupplierAddressStreet: '123 Main Street', customerSupplierAddressHouseNumber: '123A', customerSupplierAddressCity: 'Tel Aviv', customerSupplierAddressZip: '12345', customerSupplierAddressCountry: 'Israel', countryCode: 'IL', parentAccountKey: 'PARENT001', accountOpeningBalance: 10_000, totalDebits: 50_000, totalCredits: 40_000, accountingClassificationCode: 1100, supplierCustomerTaxId: '987654321', branchId: 'BR001', openingBalanceForeignCurrency: 3000, foreignCurrencyCode: 'USD', reserved: '', }; describe('B110Schema validation', () => { it('should validate a complete B110 record', () => { expect(() => B110Schema.parse(validB110)).not.toThrow(); }); it('should require code to be exactly "B110"', () => { expect(() => B110Schema.parse({ ...validB110, code: 'B11' })).toThrow(); expect(() => B110Schema.parse({ ...validB110, code: 'B1100' })).toThrow(); expect(() => B110Schema.parse({ ...validB110, code: 'A110' })).toThrow(); }); it('should require non-empty required fields', () => { expect(() => B110Schema.parse({ ...validB110, recordNumber: 0 })).toThrow(); expect(() => B110Schema.parse({ ...validB110, vatId: '' })).toThrow(); expect(() => B110Schema.parse({ ...validB110, accountKey: '' })).toThrow(); expect(() => B110Schema.parse({ ...validB110, trialBalanceCode: '' })).toThrow(); }); it('should allow empty optional fields', () => { const minimal: B110 = { code: 'B110', recordNumber: 1, vatId: '123456789', accountKey: 'ACC001', accountName: 'Cash Account', trialBalanceCode: 'TB001', trialBalanceCodeDescription: 'Current Assets', customerSupplierAddressStreet: '', customerSupplierAddressHouseNumber: '', customerSupplierAddressCity: '', customerSupplierAddressZip: '', customerSupplierAddressCountry: '', countryCode: undefined, parentAccountKey: '', accountOpeningBalance: -500, totalDebits: undefined, totalCredits: undefined, accountingClassificationCode: undefined, supplierCustomerTaxId: '', branchId: '', openingBalanceForeignCurrency: undefined, foreignCurrencyCode: undefined, reserved: '', }; expect(() => B110Schema.parse(minimal)).not.toThrow(); }); it('should validate numeric fields', () => { expect(() => B110Schema.parse({ ...validB110, recordNumber: NaN })).toThrow(); expect(() => B110Schema.parse({ ...validB110, vatId: 'abc' })).toThrow(); expect(() => B110Schema.parse({ ...validB110, accountingClassificationCode: NaN })).toThrow(); expect(() => B110Schema.parse({ ...validB110, supplierCustomerTaxId: 'abc' })).toThrow(); }); it('should enforce maximum field lengths', () => { expect(() => B110Schema.parse({ ...validB110, recordNumber: 1_000_000_000 })).toThrow(); expect(() => B110Schema.parse({ ...validB110, vatId: '1234567890' })).toThrow(); expect(() => B110Schema.parse({ ...validB110, accountKey: 'x'.repeat(16) })).toThrow(); expect(() => B110Schema.parse({ ...validB110, accountName: 'x'.repeat(51) })).toThrow(); expect(() => B110Schema.parse({ ...validB110, trialBalanceCode: 'x'.repeat(16) })).toThrow(); expect(() => B110Schema.parse({ ...validB110, trialBalanceCodeDescription: 'x'.repeat(31) }), ).toThrow(); expect(() => B110Schema.parse({ ...validB110, customerSupplierAddressStreet: 'x'.repeat(51) }), ).toThrow(); expect(() => B110Schema.parse({ ...validB110, customerSupplierAddressHouseNumber: 'x'.repeat(11) }), ).toThrow(); expect(() => B110Schema.parse({ ...validB110, customerSupplierAddressCity: 'x'.repeat(31) }), ).toThrow(); expect(() => B110Schema.parse({ ...validB110, customerSupplierAddressZip: 'x'.repeat(9) }), ).toThrow(); expect(() => B110Schema.parse({ ...validB110, customerSupplierAddressCountry: 'x'.repeat(31) }), ).toThrow(); expect(() => B110Schema.parse({ ...validB110, countryCode: 'x'.repeat(3) })).toThrow(); expect(() => B110Schema.parse({ ...validB110, parentAccountKey: 'x'.repeat(16) })).toThrow(); expect(() => B110Schema.parse({ ...validB110, accountOpeningBalance: 'x'.repeat(16) }), ).toThrow(); expect(() => B110Schema.parse({ ...validB110, totalDebits: 'x'.repeat(16) })).toThrow(); expect(() => B110Schema.parse({ ...validB110, totalCredits: 'x'.repeat(16) })).toThrow(); expect(() => B110Schema.parse({ ...validB110, accountingClassificationCode: 'x'.repeat(5) }), ).toThrow(); expect(() => B110Schema.parse({ ...validB110, supplierCustomerTaxId: 'x'.repeat(10) }), ).toThrow(); expect(() => B110Schema.parse({ ...validB110, branchId: 'x'.repeat(8) })).toThrow(); expect(() => B110Schema.parse({ ...validB110, openingBalanceForeignCurrency: 'x'.repeat(16) }), ).toThrow(); expect(() => B110Schema.parse({ ...validB110, foreignCurrencyCode: 'x'.repeat(4) }), ).toThrow(); expect(() => B110Schema.parse({ ...validB110, reserved: 'x'.repeat(17) })).toThrow(); }); }); describe('encodeB110', () => { it('should encode a valid B110 record to fixed-width format', () => { const encoded = encodeB110(validB110); // Should end with CRLF expect(encoded).toMatch(/\r\n$/); // Remove CRLF for length check const withoutCrlf = encoded.replace(/\r\n$/, ''); expect(withoutCrlf).toHaveLength(376); // Check specific field positions expect(withoutCrlf.slice(0, 4)).toBe('B110'); // 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, 37)).toBe('ACC001 '); // Account key (left-aligned) expect(withoutCrlf.slice(37, 87)).toBe('Cash Account '); // Account name (left-aligned) }); it('should handle numeric fields with zero padding', () => { const shortFields: B110 = { ...validB110, recordNumber: 42, vatId: '12345', accountingClassificationCode: 1, supplierCustomerTaxId: '99', }; const encoded = encodeB110(shortFields); const withoutCrlf = encoded.replace(/\r\n$/, ''); // Record number should be zero-padded expect(withoutCrlf.slice(4, 13)).toBe('000000042'); // VAT ID should be zero-padded expect(withoutCrlf.slice(13, 22)).toBe('000012345'); // Accounting classification code should be zero-padded (at position calculated from spec) const accClassPos = 4 + 9 + 9 + 15 + 50 + 15 + 30 + 50 + 10 + 30 + 8 + 30 + 2 + 15 + 15 + 15 + 15; expect(withoutCrlf.slice(accClassPos, accClassPos + 4)).toBe('0001'); // Supplier/Customer tax ID should be zero-padded const supplierTaxPos = accClassPos + 4; expect(withoutCrlf.slice(supplierTaxPos, supplierTaxPos + 9)).toBe('99 '); }); it('should handle amount fields with monetary format', () => { const encoded = encodeB110(validB110); const withoutCrlf = encoded.replace(/\r\n$/, ''); // Find account opening balance field position const openingBalancePos = 4 + 9 + 9 + 15 + 50 + 15 + 30 + 50 + 10 + 30 + 8 + 30 + 2 + 15; expect(withoutCrlf.slice(openingBalancePos, openingBalancePos + 15)).toBe('+00000001000000'); // 15-char monetary format // Find total debits field position const totalDebitsPos = openingBalancePos + 15; expect(withoutCrlf.slice(totalDebitsPos, totalDebitsPos + 15)).toBe('+00000005000000'); // 15-char monetary format // Find total credits field position const totalCreditsPos = totalDebitsPos + 15; expect(withoutCrlf.slice(totalCreditsPos, totalCreditsPos + 15)).toBe('+00000004000000'); // 15-char monetary format }); it('should pad empty fields correctly', () => { const minimal: B110 = { code: 'B110', recordNumber: 1, vatId: '123456789', accountKey: 'ACC001', accountName: 'Cash Account', trialBalanceCode: 'TB001', trialBalanceCodeDescription: 'Current Assets', customerSupplierAddressStreet: '', customerSupplierAddressHouseNumber: '', customerSupplierAddressCity: '', customerSupplierAddressZip: '', customerSupplierAddressCountry: '', countryCode: undefined, parentAccountKey: '', accountOpeningBalance: 0, totalDebits: undefined, totalCredits: undefined, accountingClassificationCode: undefined, supplierCustomerTaxId: '', branchId: '', openingBalanceForeignCurrency: undefined, foreignCurrencyCode: undefined, reserved: '', }; const encoded = encodeB110(minimal); const withoutCrlf = encoded.replace(/\r\n$/, ''); // Should still be exactly 376 characters expect(withoutCrlf).toHaveLength(376); // Empty accounting classification code should be zero-padded const accClassPos = 4 + 9 + 9 + 15 + 50 + 15 + 30 + 50 + 10 + 30 + 8 + 30 + 2 + 15 + 15 + 15 + 15; expect(withoutCrlf.slice(accClassPos, accClassPos + 4)).toBe('0000'); // Empty supplier/customer tax ID should be zero-padded const supplierTaxPos = accClassPos + 4; expect(withoutCrlf.slice(supplierTaxPos, supplierTaxPos + 9)).toBe(' '); }); }); describe('parseB110', () => { it('should parse a valid encoded B110 record', () => { const encoded = encodeB110(validB110); const parsed = parseB110(encoded); expect(parsed.code).toBe('B110'); expect(parsed.recordNumber).toBe(1); expect(parsed.vatId).toBe('123456789'); expect(parsed.accountKey).toBe('ACC001'); expect(parsed.accountName).toBe('Cash Account'); expect(parsed.trialBalanceCode).toBe('TB001'); expect(parsed.trialBalanceCodeDescription).toBe('Current Assets'); expect(parsed.customerSupplierAddressStreet).toBe('123 Main Street'); expect(parsed.accountOpeningBalance).toBe(10_000); expect(parsed.totalDebits).toBe(50_000); expect(parsed.totalCredits).toBe(40_000); }); it('should handle lines without CRLF', () => { const encoded = encodeB110(validB110); const withoutCrlf = encoded.replace(/\r\n$/, ''); const parsed = parseB110(withoutCrlf); expect(parsed.code).toBe(validB110.code); expect(parsed.recordNumber).toBe(validB110.recordNumber); }); it('should throw on invalid line length', () => { const shortLine = 'B110 1123456789'; expect(() => parseB110(shortLine)).toThrow('Invalid B110 record length'); }); it('should throw on invalid record code', () => { const invalidCode = 'A110' + ' '.repeat(372); expect(() => parseB110(invalidCode)).toThrow('Invalid B110 record code'); }); it('should strip leading zeros from numeric fields correctly', () => { // Create a line with zero-padded numeric fields const paddedLine = 'B110' + // code (4) '000000042' + // recordNumber (9) '000000123' + // vatId (9) 'ACC001 ' + // accountKey (15) 'Cash Account ' + // accountName (50) 'TB001 ' + // trialBalanceCode (15) 'Current Assets ' + // trialBalanceCodeDescription (30) ' '.repeat(50) + // customerSupplierAddressStreet (50) ' '.repeat(10) + // customerSupplierAddressHouseNumber (10) ' '.repeat(30) + // customerSupplierAddressCity (30) ' '.repeat(8) + // customerSupplierAddressZip (8) ' '.repeat(30) + // customerSupplierAddressCountry (30) ' '.repeat(2) + // countryCode (2) ' '.repeat(15) + // parentAccountKey (15) '-00000000123400' + // accountOpeningBalance (15) ' '.repeat(15) + // totalDebits (15) ' '.repeat(15) + // totalCredits (15) '0099' + // accountingClassificationCode (4) '000000456' + // supplierCustomerTaxId (9) ' '.repeat(7) + // branchId (7) ' '.repeat(15) + // openingBalanceForeignCurrency (15) ' '.repeat(3) + // foreignCurrencyCode (3) ' '.repeat(16); // reserved (16) expect(paddedLine.length).toBe(376); // Verify correct length const parsed = parseB110(paddedLine); expect(parsed.recordNumber).toBe(42); expect(parsed.vatId).toBe('123'); expect(parsed.accountingClassificationCode).toBe(99); expect(parsed.supplierCustomerTaxId).toBe('456'); }); it('should trim whitespace from parsed fields', () => { const encoded = encodeB110(validB110); const parsed = parseB110(encoded); // All string fields should be trimmed expect(parsed.accountName).toBe('Cash Account'); expect(parsed.trialBalanceCode).toBe('TB001'); expect(parsed.trialBalanceCodeDescription).toBe('Current Assets'); expect(parsed.customerSupplierAddressStreet).toBe('123 Main Street'); expect(parsed.accountKey).toBe('ACC001'); }); }); describe('round-trip encoding/parsing', () => { it('should maintain data integrity through encode/parse cycle', () => { const original = validB110; const encoded = encodeB110(original); const parsed = parseB110(encoded); expect(parsed).toEqual(original); }); it('should work with various field values', () => { const testCases: B110[] = [ { ...validB110, recordNumber: 999_999_999, vatId: '999999999', accountingClassificationCode: undefined, supplierCustomerTaxId: '999999999', }, { ...validB110, recordNumber: 1, vatId: '1', customerSupplierAddressStreet: '', customerSupplierAddressHouseNumber: '', customerSupplierAddressCity: '', customerSupplierAddressZip: '', customerSupplierAddressCountry: '', countryCode: undefined, parentAccountKey: '', accountOpeningBalance: 0, totalDebits: 0, totalCredits: 0, accountingClassificationCode: undefined, supplierCustomerTaxId: '', branchId: '', openingBalanceForeignCurrency: 0, foreignCurrencyCode: undefined, reserved: '', }, ]; for (const testCase of testCases) { const encoded = encodeB110(testCase); const parsed = parseB110(encoded); expect(parsed).toEqual(testCase); } }); it('should preserve empty optional fields', () => { const withEmptyFields: B110 = { ...validB110, customerSupplierAddressStreet: '', customerSupplierAddressHouseNumber: '', customerSupplierAddressCity: '', customerSupplierAddressZip: '', customerSupplierAddressCountry: '', countryCode: undefined, parentAccountKey: '', accountOpeningBalance: 0, totalDebits: 0, totalCredits: 0, accountingClassificationCode: undefined, supplierCustomerTaxId: '', branchId: '', openingBalanceForeignCurrency: 0, foreignCurrencyCode: undefined, reserved: '', }; const encoded = encodeB110(withEmptyFields); const parsed = parseB110(encoded); expect(parsed.customerSupplierAddressStreet).toBe(''); expect(parsed.accountOpeningBalance).toBe(0); expect(parsed.reserved).toBe(''); expect(parsed).toEqual(withEmptyFields); }); }); describe('error cases', () => { it('should handle malformed input gracefully', () => { expect(() => parseB110('')).toThrow(); expect(() => parseB110('invalid')).toThrow(); expect(() => parseB110('B' + ' '.repeat(500))).toThrow(); }); it('should validate against schema after parsing', () => { // Create a line with empty required fields const invalidLine = 'B110' + '000000001' + '123456789' + ' '.repeat(15) + // Empty account key (required) ' '.repeat(50) + ' '.repeat(15) + ' '.repeat(30) + ' '.repeat(50) + ' '.repeat(10) + ' '.repeat(30) + ' '.repeat(8) + ' '.repeat(30) + ' '.repeat(2) + ' '.repeat(15) + ' '.repeat(15) + ' '.repeat(15) + ' '.repeat(15) + '0000' + '000000000' + ' '.repeat(7) + ' '.repeat(15) + ' '.repeat(3) + ' '.repeat(16); expect(() => parseB110(invalidLine)).toThrow(); }); }); });