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