@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
text/typescript
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();
});
});
});