@accounter/shaam-uniform-format-generator
Version:
Fully typed application that generates, parses, and validates SHAAM uniform format tax reports (INI.TXT and BKMVDATA.TXT).
319 lines (271 loc) • 11.6 kB
text/typescript
import { beforeEach, describe, expect, it } from 'vitest';
import { SHAAM_VERSION } from '../../src/constants.js';
import {
A000Schema,
encodeA000,
parseA000,
type A000,
type A000Input,
} from '../../src/generator/records/a000.js';
import { defaultKeyGenerator } from '../../src/utils/key-generator.js';
describe('A000 Record', () => {
// Reset key generator before each test for consistent results
beforeEach(() => {
defaultKeyGenerator.reset();
});
const validA000Input: A000Input = {
reservedFuture: '',
totalRecords: '12345',
vatId: '123456789',
softwareRegNumber: '87654321',
softwareName: 'Test Software',
softwareVersion: '1.0.0',
vendorVatId: '987654321',
vendorName: 'Test Vendor',
softwareType: '1',
fileOutputPath: '/path/to/output',
accountingType: '2',
balanceRequired: '1',
companyRegId: '111222333',
withholdingFileNum: '444555666',
reserved1017: '',
businessName: 'Test Business Ltd',
businessStreet: 'Main Street 123',
businessHouseNum: '123',
businessCity: 'Tel Aviv',
businessZip: '12345',
taxYear: '2024',
startDate: '20240101',
endDate: '20241231',
processStartDate: '20240115',
processStartTime: '1430',
languageCode: '0',
characterEncoding: '1',
compressionSoftware: 'WinRAR',
reserved1031: '',
baseCurrency: 'ILS',
reserved1033: '',
branchInfoFlag: '0',
reserved1035: '',
};
describe('Schema Validation', () => {
it('should validate a valid A000 record', () => {
const fullRecord: A000 = {
code: 'A000',
primaryIdentifier: '123456789012345',
systemConstant: SHAAM_VERSION,
...validA000Input,
};
expect(() => A000Schema.parse(fullRecord)).not.toThrow();
});
it('should reject invalid record code', () => {
const invalidRecord = {
...validA000Input,
code: 'B000', // Invalid code
primaryIdentifier: '123456789012345',
systemConstant: SHAAM_VERSION,
};
expect(() => A000Schema.parse(invalidRecord)).toThrow();
});
it('should reject invalid software type', () => {
const invalidRecord = {
...validA000Input,
code: 'A000',
primaryIdentifier: '123456789012345',
systemConstant: SHAAM_VERSION,
softwareType: '3', // Invalid software type
};
expect(() => A000Schema.parse(invalidRecord)).toThrow();
});
it('should reject invalid language code', () => {
const invalidRecord = {
...validA000Input,
code: 'A000',
primaryIdentifier: '123456789012345',
systemConstant: SHAAM_VERSION,
languageCode: '9', // Invalid language code
};
expect(() => A000Schema.parse(invalidRecord)).toThrow();
});
it('should reject fields that exceed maximum length', () => {
const invalidRecord = {
...validA000Input,
code: 'A000',
primaryIdentifier: '123456789012345',
systemConstant: SHAAM_VERSION,
softwareName: 'A'.repeat(21), // Exceeds 20 character limit
};
expect(() => A000Schema.parse(invalidRecord)).toThrow();
});
});
describe('encodeA000', () => {
it('should encode a valid A000 record to fixed-width string', () => {
const encoded = encodeA000(validA000Input);
// Should end with CRLF
expect(encoded).toMatch(/\r\n$/);
// Should have correct length (466 + 2 for CRLF)
expect(encoded.length).toBe(468);
// Should start with A000
expect(encoded.slice(0, 4)).toBe('A000');
// Should contain the system constant at the correct position (offset 48-55)
expect(encoded.slice(48, 56)).toBe(SHAAM_VERSION);
});
it('should pad numeric fields with leading zeros', () => {
const encoded = encodeA000(validA000Input);
// VAT ID should be zero-padded to 9 digits (position 24-32)
const vatIdField = encoded.slice(24, 33);
expect(vatIdField).toBe('123456789');
// Total records should be zero-padded to 15 digits (position 9-23)
const totalRecordsField = encoded.slice(9, 24);
expect(totalRecordsField).toBe('000000000012345');
});
it('should handle empty optional fields', () => {
const inputWithEmptyFields: A000Input = {
...validA000Input,
companyRegId: '',
withholdingFileNum: '',
businessStreet: '',
compressionSoftware: '',
};
const encoded = encodeA000(inputWithEmptyFields);
expect(encoded.length).toBe(468); // Should still have correct total length
});
});
describe('parseA000', () => {
it('should parse a valid fixed-width A000 record string', () => {
const encoded = encodeA000(validA000Input);
const parsed = parseA000(encoded);
expect(parsed.code).toBe('A000');
expect(parsed.vatId).toBe('123456789');
expect(parsed.totalRecords).toBe('12345');
expect(parsed.softwareName).toBe('Test Software');
expect(parsed.businessName).toBe('Test Business Ltd');
expect(parsed.systemConstant).toBe(SHAAM_VERSION);
});
it('should handle lines with and without CRLF', () => {
const encoded = encodeA000(validA000Input);
const withoutCRLF = encoded.replace(/\r\n$/, '');
expect(() => parseA000(encoded)).not.toThrow();
expect(() => parseA000(withoutCRLF)).not.toThrow();
const parsedWithCRLF = parseA000(encoded);
const parsedWithoutCRLF = parseA000(withoutCRLF);
expect(parsedWithCRLF).toEqual(parsedWithoutCRLF);
});
it('should throw error for invalid record length', () => {
const shortLine = 'A000' + ' '.repeat(100);
const longLine = 'A000' + ' '.repeat(600);
expect(() => parseA000(shortLine)).toThrow(
'Invalid A000 record length: expected 466 characters, got 104',
);
expect(() => parseA000(longLine)).toThrow(
'Invalid A000 record length: expected 466 characters, got 604',
);
});
it('should throw error for invalid record code', () => {
const validEncoded = encodeA000(validA000Input);
const invalidLine = 'B000' + validEncoded.slice(4);
expect(() => parseA000(invalidLine)).toThrow(
'Invalid A000 record code: expected "A000", got "B000"',
);
});
it('should throw error for invalid system constant', () => {
const validEncoded = encodeA000(validA000Input);
// Replace system constant with invalid value (at position 48-55)
const invalidLine = validEncoded.slice(0, 48) + 'INVALID ' + validEncoded.slice(56);
expect(() => parseA000(invalidLine)).toThrow(/Invalid system constant/);
});
it('should properly strip leading zeros from numeric fields', () => {
const encoded = encodeA000(validA000Input);
const parsed = parseA000(encoded);
// These should not have leading zeros after parsing
expect(parsed.vatId).toBe('123456789');
expect(parsed.totalRecords).toBe('12345');
expect(parsed.primaryIdentifier).not.toMatch(/^0+/);
});
});
describe('Round-trip Tests', () => {
it('should maintain data integrity through encode-parse round trip', () => {
const encoded = encodeA000(validA000Input);
const parsed = parseA000(encoded);
const reEncoded = encodeA000(parsed);
expect(reEncoded).toBe(encoded);
});
it('should preserve all field values in round trip', () => {
const encoded = encodeA000(validA000Input);
const parsed = parseA000(encoded);
// Compare all input fields (excluding auto-generated ones)
expect(parsed.reservedFuture).toBe(validA000Input.reservedFuture);
expect(parsed.totalRecords).toBe(validA000Input.totalRecords);
expect(parsed.vatId).toBe(validA000Input.vatId);
expect(parsed.softwareRegNumber).toBe(validA000Input.softwareRegNumber);
expect(parsed.softwareName).toBe(validA000Input.softwareName);
expect(parsed.softwareVersion).toBe(validA000Input.softwareVersion);
expect(parsed.vendorVatId).toBe(validA000Input.vendorVatId);
expect(parsed.vendorName).toBe(validA000Input.vendorName);
expect(parsed.softwareType).toBe(validA000Input.softwareType);
expect(parsed.fileOutputPath).toBe(validA000Input.fileOutputPath);
expect(parsed.accountingType).toBe(validA000Input.accountingType);
expect(parsed.balanceRequired).toBe(validA000Input.balanceRequired);
expect(parsed.companyRegId).toBe(validA000Input.companyRegId);
expect(parsed.withholdingFileNum).toBe(validA000Input.withholdingFileNum);
expect(parsed.reserved1017).toBe(validA000Input.reserved1017);
expect(parsed.businessName).toBe(validA000Input.businessName);
expect(parsed.businessStreet).toBe(validA000Input.businessStreet);
expect(parsed.businessHouseNum).toBe(validA000Input.businessHouseNum);
expect(parsed.businessCity).toBe(validA000Input.businessCity);
expect(parsed.businessZip).toBe(validA000Input.businessZip);
expect(parsed.taxYear).toBe(validA000Input.taxYear);
expect(parsed.startDate).toBe(validA000Input.startDate);
expect(parsed.endDate).toBe(validA000Input.endDate);
expect(parsed.processStartDate).toBe(validA000Input.processStartDate);
expect(parsed.processStartTime).toBe(validA000Input.processStartTime);
expect(parsed.languageCode).toBe(validA000Input.languageCode);
expect(parsed.characterEncoding).toBe(validA000Input.characterEncoding);
expect(parsed.compressionSoftware).toBe(validA000Input.compressionSoftware);
expect(parsed.reserved1031).toBe(validA000Input.reserved1031);
expect(parsed.baseCurrency).toBe(validA000Input.baseCurrency);
expect(parsed.reserved1033).toBe(validA000Input.reserved1033);
expect(parsed.branchInfoFlag).toBe(validA000Input.branchInfoFlag);
expect(parsed.reserved1035).toBe(validA000Input.reserved1035);
});
it('should handle edge cases in round trip', () => {
const edgeCaseInput: A000Input = {
...validA000Input,
reservedFuture: 'ABCDE', // Maximum length
totalRecords: '1', // Minimum length
vatId: '1', // Minimum length
softwareName: 'A'.repeat(20), // Maximum length
businessName: 'B'.repeat(50), // Maximum length
companyRegId: '', // Empty optional field
withholdingFileNum: '', // Empty optional field
};
const encoded = encodeA000(edgeCaseInput);
const parsed = parseA000(encoded);
const reEncoded = encodeA000(parsed);
expect(reEncoded).toBe(encoded);
expect(parsed.reservedFuture).toBe('ABCDE');
expect(parsed.totalRecords).toBe('1');
expect(parsed.vatId).toBe('1');
expect(parsed.companyRegId).toBe('');
});
});
describe('Error Handling', () => {
it('should throw error when feeding invalid line to parseA000', () => {
const invalidLines = [
'', // Empty line
'A000', // Too short
'A000' + ' '.repeat(600), // Too long
'XXXX' + ' '.repeat(533), // Wrong record code
];
for (const line of invalidLines) {
expect(() => parseA000(line)).toThrow();
}
});
it('should validate schema after parsing', () => {
// Create a line with invalid software type (at position 133)
const validEncoded = encodeA000(validA000Input);
const invalidLine = validEncoded.slice(0, 133) + '9' + validEncoded.slice(134); // Invalid software type
expect(() => parseA000(invalidLine)).toThrow();
});
});
});