@accounter/shaam-uniform-format-generator
Version:
Fully typed application that generates, parses, and validates SHAAM uniform format tax reports (INI.TXT and BKMVDATA.TXT).
334 lines (290 loc) • 12.5 kB
text/typescript
import { describe, expect, it } from 'vitest';
import { C100Schema, encodeC100, parseC100, type C100 } from '../../src/generator/records/c100.js';
describe('C100 Record', () => {
const validC100: C100 = {
code: 'C100',
recordNumber: '1',
vatId: '123456789',
documentType: '300', // Will be encoded as '300' then parsed back as '300'
documentId: 'DOC123456789',
documentIssueDate: '20240315',
documentIssueTime: '1430',
customerName: 'Test Customer Ltd',
customerStreet: 'Rothschild Blvd',
customerHouseNumber: '123',
customerCity: 'Tel Aviv',
customerPostCode: '6100000',
customerCountry: 'Israel',
customerCountryCode: 'IL',
customerPhone: '03-1234567',
customerVatId: '987654321',
documentValueDate: '20240301',
foreignCurrencyAmount: '1000.00',
currencyCode: 'USD',
amountBeforeDiscount: '1200.00',
documentDiscount: '200.00',
amountAfterDiscountExcludingVat: '1000.00',
vatAmount: '170.00',
amountIncludingVat: '1170.00',
withholdingTaxAmount: '0.00',
customerKey: 'CUST001',
matchingField: 'MATCH001',
cancelledAttribute1: '',
cancelledDocument: 'N',
cancelledAttribute2: '',
documentDate: '2024031',
branchKey: 'BRANCH01',
cancelledAttribute3: '',
actionExecutor: 'USER001',
lineConnectingField: '',
reserved: '',
};
describe('C100Schema validation', () => {
it('should validate a complete C100 record', () => {
expect(() => C100Schema.parse(validC100)).not.toThrow();
});
it('should require code to be exactly "C100"', () => {
expect(() => C100Schema.parse({ ...validC100, code: 'C10' })).toThrow();
expect(() => C100Schema.parse({ ...validC100, code: 'C1000' })).toThrow();
expect(() => C100Schema.parse({ ...validC100, code: 'D100' })).toThrow();
});
it('should validate field lengths', () => {
expect(() => C100Schema.parse({ ...validC100, recordNumber: '1234567890' })).toThrow();
expect(() => C100Schema.parse({ ...validC100, vatId: '1234567890' })).toThrow();
expect(() => C100Schema.parse({ ...validC100, documentType: '1234' })).toThrow();
expect(() => C100Schema.parse({ ...validC100, documentId: 'x'.repeat(21) })).toThrow();
expect(() => C100Schema.parse({ ...validC100, customerName: 'x'.repeat(51) })).toThrow();
expect(() => C100Schema.parse({ ...validC100, customerStreet: 'x'.repeat(51) })).toThrow();
});
it('should allow empty values for optional fields', () => {
const minimalC100: C100 = {
code: 'C100',
recordNumber: '1',
vatId: '123456789',
documentType: '100', // Required according to CSV
documentId: 'DOC001', // Required according to CSV
documentIssueDate: '20240315', // Required according to CSV
documentIssueTime: '', // Optional
customerName: '',
customerStreet: '',
customerHouseNumber: '',
customerCity: '',
customerPostCode: '',
customerCountry: '',
customerCountryCode: '',
customerPhone: '',
customerVatId: '',
documentValueDate: '',
foreignCurrencyAmount: '',
currencyCode: '',
amountBeforeDiscount: '',
documentDiscount: '',
amountAfterDiscountExcludingVat: '',
vatAmount: '',
amountIncludingVat: '',
withholdingTaxAmount: '',
customerKey: '',
matchingField: '',
cancelledAttribute1: '',
cancelledDocument: '',
cancelledAttribute2: '',
documentDate: '',
branchKey: '',
cancelledAttribute3: '',
actionExecutor: '',
lineConnectingField: '',
reserved: '',
};
expect(() => C100Schema.parse(minimalC100)).not.toThrow();
});
});
describe('encodeC100', () => {
it('should encode a valid C100 record to fixed-width format', () => {
const encoded = encodeC100(validC100);
// Should end with CRLF
expect(encoded).toMatch(/\r\n$/);
// Remove CRLF for length check
const withoutCrlf = encoded.replace(/\r\n$/, '');
expect(withoutCrlf).toHaveLength(445);
// Check specific field positions
expect(withoutCrlf.slice(0, 4)).toBe('C100'); // Record code - left-aligned
expect(withoutCrlf.slice(4, 13)).toBe('000000001'); // Record number - zero-padded (numeric)
expect(withoutCrlf.slice(13, 22)).toBe('123456789'); // VAT ID - zero-padded (numeric)
expect(withoutCrlf.slice(22, 25)).toBe('300'); // Document type - zero-padded (numeric)
expect(withoutCrlf.slice(25, 45)).toBe('DOC123456789 '); // Document ID - left-aligned (alphanumeric)
});
it('should handle long fields by truncating', () => {
const longFields: C100 = {
...validC100,
vatId: '1234567890', // Too long for 9 chars
documentId: 'x'.repeat(25), // Too long for 20 chars
customerName: 'Very Long Customer Name That Exceeds The Maximum Length', // Too long for 50 chars
};
const encoded = encodeC100(longFields);
const withoutCrlf = encoded.replace(/\r\n$/, '');
// Should still be exactly 445 characters
expect(withoutCrlf).toHaveLength(445);
// VAT ID should be truncated to 9 chars and zero-padded
expect(withoutCrlf.slice(13, 22)).toBe('123456789'); // Already 9 chars when truncated from 1234567890
// Document ID should be truncated to 20 chars and left-aligned
expect(withoutCrlf.slice(25, 45)).toBe('xxxxxxxxxxxxxxxxxxxx');
});
it('should pad short fields correctly', () => {
const shortFields: C100 = {
...validC100,
recordNumber: '42',
vatId: '123',
documentId: 'SHORT',
customerName: 'Short Name',
};
const encoded = encodeC100(shortFields);
const withoutCrlf = encoded.replace(/\r\n$/, '');
// Record number should be zero-padded (numeric field)
expect(withoutCrlf.slice(4, 13)).toBe('000000042');
// VAT ID should be zero-padded (numeric field)
expect(withoutCrlf.slice(13, 22)).toBe('000000123');
// Document ID should be left-aligned with spaces (alphanumeric field)
expect(withoutCrlf.slice(25, 45)).toBe('SHORT ');
});
});
describe('parseC100', () => {
it('should parse a valid encoded C100 record', () => {
const encoded = encodeC100(validC100);
const parsed = parseC100(encoded);
expect(parsed.code).toBe('C100');
expect(parsed.recordNumber).toBe('1');
expect(parsed.vatId).toBe('123456789');
expect(parsed.documentType).toBe('300');
expect(parsed.documentId).toBe('DOC123456789');
expect(parsed.documentIssueDate).toBe('20240315');
expect(parsed.documentIssueTime).toBe('1430');
expect(parsed.customerName).toBe('Test Customer Ltd');
expect(parsed.customerStreet).toBe('Rothschild Blvd');
expect(parsed.customerCity).toBe('Tel Aviv');
});
it('should handle lines without CRLF', () => {
const encoded = encodeC100(validC100);
const withoutCrlf = encoded.replace(/\r\n$/, '');
const parsed = parseC100(withoutCrlf);
expect(parsed.code).toBe('C100');
expect(parsed.vatId).toBe('123456789');
});
it('should handle padded fields correctly', () => {
const withPadding: C100 = {
...validC100,
recordNumber: '5',
vatId: '12345',
documentId: 'DOC5',
customerName: 'Short',
};
const encoded = encodeC100(withPadding);
const parsed = parseC100(encoded);
expect(parsed.recordNumber).toBe('5');
expect(parsed.vatId).toBe('12345');
expect(parsed.documentId).toBe('DOC5');
expect(parsed.customerName).toBe('Short');
});
it('should throw error for invalid record length', () => {
expect(() => parseC100('C100SHORT')).toThrow(
'Invalid C100 record length: expected 445 characters, got 9',
);
expect(() => parseC100('C100' + ' '.repeat(100))).toThrow(
'Invalid C100 record length: expected 445 characters, got 104',
);
});
it('should throw error for invalid record code', () => {
const invalidLine = 'D100' + ' '.repeat(441);
expect(() => parseC100(invalidLine)).toThrow(
'Invalid C100 record code: expected "C100", got "D100"',
);
});
it('should validate parsed data against schema', () => {
// Create a line with invalid field data that would pass basic parsing but fail schema validation
const invalidLine = 'C100' + ' '.repeat(9) + 'x'.repeat(10) + ' '.repeat(432); // VAT ID too long
expect(() => parseC100(invalidLine)).toThrow();
});
});
describe('round-trip encoding and parsing', () => {
it('should maintain data integrity through encode-parse cycle', () => {
const testCases: C100[] = [
validC100,
{
...validC100,
recordNumber: '999999999',
documentType: '200', // Will be encoded as '200' then parsed back as '200'
cancelledDocument: 'Y',
},
{
...validC100,
documentId: 'MINIMAL', // Required field cannot be empty
customerName: '',
customerStreet: '',
},
];
for (const [index, testCase] of testCases.entries()) {
const encoded = encodeC100(testCase);
const parsed = parseC100(encoded);
expect(parsed, `Test case ${index} failed round-trip`).toEqual(testCase);
}
});
it('should handle maximum length fields', () => {
const maxLengthC100: C100 = {
code: 'C100',
recordNumber: '999999999', // 9 chars
vatId: '123456789', // 9 chars
documentType: '710', // 3 chars
documentId: 'x'.repeat(20), // 20 chars
documentIssueDate: '20241231', // 8 chars
documentIssueTime: '2359', // 4 chars
customerName: 'A'.repeat(50), // 50 chars
customerStreet: 'B'.repeat(50), // 50 chars
customerHouseNumber: 'C'.repeat(10), // 10 chars
customerCity: 'D'.repeat(30), // 30 chars
customerPostCode: 'E'.repeat(8), // 8 chars
customerCountry: 'F'.repeat(30), // 30 chars
customerCountryCode: 'GG', // 2 chars
customerPhone: 'H'.repeat(15), // 15 chars
customerVatId: '987654321', // 9 chars - numeric
documentValueDate: '20230101', // 8 chars
foreignCurrencyAmount: '999999999999.99', // 15 chars
currencyCode: 'KKK', // 3 chars
amountBeforeDiscount: '999999999999.99', // 15 chars
documentDiscount: '999999999999.99', // 15 chars
amountAfterDiscountExcludingVat: '999999999999.99', // 15 chars
vatAmount: '999999999999.99', // 15 chars
amountIncludingVat: '999999999999.99', // 15 chars
withholdingTaxAmount: '999999999.99', // 12 chars (9 digits + .99)
customerKey: 'R'.repeat(15), // 15 chars
matchingField: 'S'.repeat(10), // 10 chars
cancelledAttribute1: 'T'.repeat(8), // 8 chars
cancelledDocument: 'U', // 1 char
cancelledAttribute2: 'V'.repeat(8), // 8 chars
documentDate: '2024031', // 7 chars - numeric
branchKey: 'X'.repeat(8), // 8 chars
cancelledAttribute3: 'Y', // 1 char
actionExecutor: 'Z'.repeat(13), // 13 chars
lineConnectingField: '',
reserved: '',
};
const encoded = encodeC100(maxLengthC100);
const parsed = parseC100(encoded);
expect(parsed).toEqual(maxLengthC100);
});
});
describe('error handling', () => {
it('should provide meaningful error messages for validation failures', () => {
const invalidRecord = { ...validC100, code: 'INVALID' };
expect(() => C100Schema.parse(invalidRecord)).toThrow();
});
it('should handle empty strings in required fields', () => {
const invalidRecord = { ...validC100, recordNumber: '' };
expect(() => C100Schema.parse(invalidRecord)).toThrow();
});
it('should accept various field values', () => {
// Schema should accept these valid values
expect(() => C100Schema.parse({ ...validC100, cancelledDocument: 'Y' })).not.toThrow();
expect(() => C100Schema.parse({ ...validC100, cancelledDocument: 'N' })).not.toThrow();
expect(() => C100Schema.parse({ ...validC100, cancelledDocument: '' })).not.toThrow();
});
});
});