@accounter/shaam-uniform-format-generator
Version:
Fully typed application that generates, parses, and validates SHAAM uniform format tax reports (INI.TXT and BKMVDATA.TXT).
438 lines (393 loc) • 18.6 kB
text/typescript
import { describe, expect, it } from 'vitest';
import { encodeM100, M100Schema, parseM100, type M100 } from '../../src/generator/records/m100.js';
describe('M100 Record', () => {
const validM100: M100 = {
code: 'M100',
recordNumber: '123',
vatId: '987654321',
universalItemCode: 'UNI123456789',
supplierItemCode: 'SUP123456789',
internalItemCode: 'INT123456789',
itemName: 'Test Item Name',
classificationCode: 'CLASS123',
classificationDescription: 'Test Classification',
unitOfMeasure: 'יחידה',
openingStock: '100.50',
totalStockIn: '250.75',
totalStockOut: '150.25',
endPeriodCostNonBonded: '1234.56',
endPeriodCostBonded: '5678.90',
reserved: '',
};
describe('M100Schema validation', () => {
it('should validate a correct M100 record', () => {
expect(() => M100Schema.parse(validM100)).not.toThrow();
});
it('should require code to be exactly "M100"', () => {
const invalidRecord = { ...validM100, code: 'M200' };
expect(() => M100Schema.parse(invalidRecord)).toThrow();
});
it('should require recordNumber to be non-empty and max 9 characters', () => {
expect(() => M100Schema.parse({ ...validM100, recordNumber: '' })).toThrow();
expect(() => M100Schema.parse({ ...validM100, recordNumber: '1234567890' })).toThrow();
expect(() => M100Schema.parse({ ...validM100, recordNumber: '123456789' })).not.toThrow();
});
it('should require vatId to be non-empty and max 9 characters', () => {
expect(() => M100Schema.parse({ ...validM100, vatId: '' })).toThrow();
expect(() => M100Schema.parse({ ...validM100, vatId: '1234567890' })).toThrow();
expect(() => M100Schema.parse({ ...validM100, vatId: '123456789' })).not.toThrow();
});
it('should require internalItemCode to be non-empty and max 20 characters', () => {
expect(() => M100Schema.parse({ ...validM100, internalItemCode: '' })).toThrow();
expect(() => M100Schema.parse({ ...validM100, internalItemCode: 'A'.repeat(21) })).toThrow();
expect(() =>
M100Schema.parse({ ...validM100, internalItemCode: 'A'.repeat(20) }),
).not.toThrow();
});
it('should require itemName to be non-empty and max 50 characters', () => {
expect(() => M100Schema.parse({ ...validM100, itemName: '' })).toThrow();
expect(() => M100Schema.parse({ ...validM100, itemName: 'A'.repeat(51) })).toThrow();
expect(() => M100Schema.parse({ ...validM100, itemName: 'A'.repeat(50) })).not.toThrow();
});
it('should require openingStock to be non-empty and max 12 characters', () => {
expect(() => M100Schema.parse({ ...validM100, openingStock: '' })).toThrow();
expect(() => M100Schema.parse({ ...validM100, openingStock: 'A'.repeat(13) })).toThrow();
expect(() => M100Schema.parse({ ...validM100, openingStock: 'A'.repeat(12) })).not.toThrow();
});
it('should require totalStockIn to be non-empty and max 12 characters', () => {
expect(() => M100Schema.parse({ ...validM100, totalStockIn: '' })).toThrow();
expect(() => M100Schema.parse({ ...validM100, totalStockIn: 'A'.repeat(13) })).toThrow();
expect(() => M100Schema.parse({ ...validM100, totalStockIn: 'A'.repeat(12) })).not.toThrow();
});
it('should require totalStockOut to be non-empty and max 12 characters', () => {
expect(() => M100Schema.parse({ ...validM100, totalStockOut: '' })).toThrow();
expect(() => M100Schema.parse({ ...validM100, totalStockOut: 'A'.repeat(13) })).toThrow();
expect(() => M100Schema.parse({ ...validM100, totalStockOut: 'A'.repeat(12) })).not.toThrow();
});
it('should validate optional fields with proper length limits', () => {
expect(() =>
M100Schema.parse({ ...validM100, universalItemCode: 'A'.repeat(20) }),
).not.toThrow();
expect(() => M100Schema.parse({ ...validM100, universalItemCode: 'A'.repeat(21) })).toThrow();
expect(() =>
M100Schema.parse({ ...validM100, supplierItemCode: 'A'.repeat(20) }),
).not.toThrow();
expect(() => M100Schema.parse({ ...validM100, supplierItemCode: 'A'.repeat(21) })).toThrow();
expect(() =>
M100Schema.parse({ ...validM100, classificationCode: 'A'.repeat(10) }),
).not.toThrow();
expect(() =>
M100Schema.parse({ ...validM100, classificationCode: 'A'.repeat(11) }),
).toThrow();
expect(() =>
M100Schema.parse({ ...validM100, classificationDescription: 'A'.repeat(30) }),
).not.toThrow();
expect(() =>
M100Schema.parse({ ...validM100, classificationDescription: 'A'.repeat(31) }),
).toThrow();
expect(() => M100Schema.parse({ ...validM100, unitOfMeasure: 'A'.repeat(20) })).not.toThrow();
expect(() => M100Schema.parse({ ...validM100, unitOfMeasure: 'A'.repeat(21) })).toThrow();
expect(() =>
M100Schema.parse({ ...validM100, endPeriodCostNonBonded: 'A'.repeat(8) }),
).not.toThrow();
expect(() =>
M100Schema.parse({ ...validM100, endPeriodCostNonBonded: 'A'.repeat(9) }),
).toThrow();
expect(() =>
M100Schema.parse({ ...validM100, endPeriodCostBonded: 'A'.repeat(8) }),
).not.toThrow();
expect(() =>
M100Schema.parse({ ...validM100, endPeriodCostBonded: 'A'.repeat(9) }),
).toThrow();
expect(() => M100Schema.parse({ ...validM100, reserved: 'A'.repeat(50) })).not.toThrow();
expect(() => M100Schema.parse({ ...validM100, reserved: 'A'.repeat(51) })).toThrow();
});
it('should set default empty string for optional fields when missing', () => {
const minimalM100: M100 = {
code: 'M100',
recordNumber: '1',
vatId: '1',
universalItemCode: '',
supplierItemCode: '',
internalItemCode: 'INT1',
itemName: 'Item',
classificationCode: '',
classificationDescription: '',
unitOfMeasure: '',
openingStock: '0',
totalStockIn: '0',
totalStockOut: '0',
endPeriodCostNonBonded: '',
endPeriodCostBonded: '',
reserved: '',
};
expect(() => M100Schema.parse(minimalM100)).not.toThrow();
});
});
describe('encodeM100', () => {
it('should encode a M100 record to fixed-width format', () => {
const encoded = encodeM100(validM100);
// Should end with CRLF
expect(encoded).toMatch(/\r\n$/);
// Remove CRLF for length check
const withoutCRLF = encoded.replace(/\r\n$/, '');
expect(withoutCRLF).toHaveLength(294);
// Check specific field positions
expect(withoutCRLF.slice(0, 4)).toBe('M100'); // code (4)
expect(withoutCRLF.slice(4, 13)).toBe('000000123'); // recordNumber zero-padded (9)
expect(withoutCRLF.slice(13, 22)).toBe('987654321'); // vatId zero-padded (9)
expect(withoutCRLF.slice(22, 42)).toBe('UNI123456789 '); // universalItemCode left-aligned (20)
expect(withoutCRLF.slice(42, 62)).toBe('SUP123456789 '); // supplierItemCode left-aligned (20)
expect(withoutCRLF.slice(62, 82)).toBe('INT123456789 '); // internalItemCode left-aligned (20)
expect(withoutCRLF.slice(82, 132)).toBe('Test Item Name' + ' '.repeat(36)); // itemName left-aligned (50)
expect(withoutCRLF.slice(132, 142)).toBe('CLASS123 '); // classificationCode left-aligned (10)
expect(withoutCRLF.slice(142, 172)).toBe('Test Classification' + ' '.repeat(11)); // classificationDescription left-aligned (30)
expect(withoutCRLF.slice(172, 192)).toBe('יחידה' + ' '.repeat(15)); // unitOfMeasure left-aligned (20)
expect(withoutCRLF.slice(192, 204)).toBe('100.50 '); // openingStock left-aligned (12)
expect(withoutCRLF.slice(204, 216)).toBe('250.75 '); // totalStockIn left-aligned (12)
expect(withoutCRLF.slice(216, 228)).toBe('150.25 '); // totalStockOut left-aligned (12)
expect(withoutCRLF.slice(228, 236)).toBe('1234.56 '); // endPeriodCostNonBonded left-aligned (8)
expect(withoutCRLF.slice(236, 244)).toBe('5678.90 '); // endPeriodCostBonded left-aligned (8)
expect(withoutCRLF.slice(244, 294)).toBe(' '.repeat(50)); // reserved left-aligned (50)
});
it('should handle maximum length fields', () => {
const maxLengthRecord: M100 = {
code: 'M100',
recordNumber: '123456789', // 9 chars
vatId: '123456789', // 9 chars
universalItemCode: 'A'.repeat(20), // 20 chars
supplierItemCode: 'B'.repeat(20), // 20 chars
internalItemCode: 'C'.repeat(20), // 20 chars
itemName: 'D'.repeat(50), // 50 chars
classificationCode: 'E'.repeat(10), // 10 chars
classificationDescription: 'F'.repeat(30), // 30 chars
unitOfMeasure: 'G'.repeat(20), // 20 chars
openingStock: 'H'.repeat(12), // 12 chars
totalStockIn: 'I'.repeat(12), // 12 chars
totalStockOut: 'J'.repeat(12), // 12 chars
endPeriodCostNonBonded: 'K'.repeat(8), // 8 chars
endPeriodCostBonded: 'L'.repeat(8), // 8 chars
reserved: 'M'.repeat(50), // 50 chars
};
const encoded = encodeM100(maxLengthRecord);
const withoutCRLF = encoded.replace(/\r\n$/, '');
expect(withoutCRLF).toHaveLength(294);
expect(withoutCRLF.slice(0, 4)).toBe('M100');
expect(withoutCRLF.slice(4, 13)).toBe('123456789');
expect(withoutCRLF.slice(13, 22)).toBe('123456789');
expect(withoutCRLF.slice(22, 42)).toBe('A'.repeat(20));
expect(withoutCRLF.slice(42, 62)).toBe('B'.repeat(20));
expect(withoutCRLF.slice(62, 82)).toBe('C'.repeat(20));
expect(withoutCRLF.slice(82, 132)).toBe('D'.repeat(50));
expect(withoutCRLF.slice(132, 142)).toBe('E'.repeat(10));
expect(withoutCRLF.slice(142, 172)).toBe('F'.repeat(30));
expect(withoutCRLF.slice(172, 192)).toBe('G'.repeat(20));
expect(withoutCRLF.slice(192, 204)).toBe('H'.repeat(12));
expect(withoutCRLF.slice(204, 216)).toBe('I'.repeat(12));
expect(withoutCRLF.slice(216, 228)).toBe('J'.repeat(12));
expect(withoutCRLF.slice(228, 236)).toBe('K'.repeat(8));
expect(withoutCRLF.slice(236, 244)).toBe('L'.repeat(8));
expect(withoutCRLF.slice(244, 294)).toBe('M'.repeat(50));
});
it('should truncate fields that exceed maximum length', () => {
const oversizedRecord: M100 = {
code: 'M100',
recordNumber: '1234567890', // 10 chars (max 9)
vatId: '1234567890', // 10 chars (max 9)
universalItemCode: 'A'.repeat(25), // 25 chars (max 20)
supplierItemCode: 'B'.repeat(25), // 25 chars (max 20)
internalItemCode: 'C'.repeat(25), // 25 chars (max 20)
itemName: 'D'.repeat(55), // 55 chars (max 50)
classificationCode: 'E'.repeat(15), // 15 chars (max 10)
classificationDescription: 'F'.repeat(35), // 35 chars (max 30)
unitOfMeasure: 'G'.repeat(25), // 25 chars (max 20)
openingStock: 'H'.repeat(15), // 15 chars (max 12)
totalStockIn: 'I'.repeat(15), // 15 chars (max 12)
totalStockOut: 'J'.repeat(15), // 15 chars (max 12)
endPeriodCostNonBonded: 'K'.repeat(10), // 10 chars (max 8)
endPeriodCostBonded: 'L'.repeat(10), // 10 chars (max 8)
reserved: 'M'.repeat(60), // 60 chars (max 50)
};
const encoded = encodeM100(oversizedRecord);
const withoutCRLF = encoded.replace(/\r\n$/, '');
expect(withoutCRLF).toHaveLength(294);
expect(withoutCRLF.slice(4, 13)).toBe('123456789'); // truncated from right
expect(withoutCRLF.slice(13, 22)).toBe('123456789'); // truncated
expect(withoutCRLF.slice(22, 42)).toBe('A'.repeat(20)); // truncated
expect(withoutCRLF.slice(244, 294)).toBe('M'.repeat(50)); // truncated
});
});
describe('parseM100', () => {
it('should parse a valid M100 record line', () => {
// Use the actual encoded output from our valid record
const encoded = encodeM100(validM100);
const parsed = parseM100(encoded);
expect(parsed).toEqual(validM100);
});
it('should parse line without CRLF', () => {
// Create a minimal valid record
const minimalRecord: M100 = {
code: 'M100',
recordNumber: '123',
vatId: '987654321',
universalItemCode: '',
supplierItemCode: '',
internalItemCode: 'INT123456789',
itemName: 'Item Name',
classificationCode: '',
classificationDescription: '',
unitOfMeasure: '',
openingStock: '100',
totalStockIn: '200',
totalStockOut: '300',
endPeriodCostNonBonded: '',
endPeriodCostBonded: '',
reserved: '',
};
const encoded = encodeM100(minimalRecord);
const lineWithoutCRLF = encoded.replace(/\r\n$/, '');
const parsed = parseM100(lineWithoutCRLF);
expect(parsed).toEqual(minimalRecord);
});
it('should throw error for invalid line length', () => {
const shortLine = 'M100' + ' '.repeat(50);
expect(() => parseM100(shortLine)).toThrow(
'Invalid M100 record length: expected 294 characters, got 54',
);
const longLine = 'M100' + ' '.repeat(400);
expect(() => parseM100(longLine)).toThrow(
'Invalid M100 record length: expected 294 characters, got 404',
);
});
it('should throw error for invalid record code', () => {
const invalidLine = 'M200' + ' '.repeat(290) + '\r\n';
expect(() => parseM100(invalidLine)).toThrow(
'Invalid M100 record code: expected "M100", got "M200"',
);
});
it('should handle fields with leading zeros correctly', () => {
// Create a record with numeric fields that might have leading zeros
const recordWithLeadingZeros: M100 = {
code: 'M100',
recordNumber: '123',
vatId: '000123456',
universalItemCode: '',
supplierItemCode: '',
internalItemCode: 'INT123456789',
itemName: 'Item Name',
classificationCode: '',
classificationDescription: '',
unitOfMeasure: '',
openingStock: '100',
totalStockIn: '200',
totalStockOut: '300',
endPeriodCostNonBonded: '00001234',
endPeriodCostBonded: '00005678',
reserved: '',
};
const encoded = encodeM100(recordWithLeadingZeros);
const parsed = parseM100(encoded);
// After parsing, leading zeros should be removed from numeric fields
expect(parsed.vatId).toBe('123456');
expect(parsed.endPeriodCostNonBonded).toBe('1234');
expect(parsed.endPeriodCostBonded).toBe('5678');
});
it('should handle empty optional fields correctly', () => {
const emptyFieldsRecord: M100 = {
code: 'M100',
recordNumber: '123',
vatId: '000123456',
universalItemCode: '',
supplierItemCode: '',
internalItemCode: 'INT123456789',
itemName: 'Item Name',
classificationCode: '',
classificationDescription: '',
unitOfMeasure: '',
openingStock: '100',
totalStockIn: '200',
totalStockOut: '300',
endPeriodCostNonBonded: '',
endPeriodCostBonded: '',
reserved: '',
};
const encoded = encodeM100(emptyFieldsRecord);
const parsed = parseM100(encoded);
expect(parsed.universalItemCode).toBe('');
expect(parsed.supplierItemCode).toBe('');
expect(parsed.classificationCode).toBe('');
expect(parsed.classificationDescription).toBe('');
expect(parsed.unitOfMeasure).toBe('');
expect(parsed.endPeriodCostNonBonded).toBe('');
expect(parsed.endPeriodCostBonded).toBe('');
expect(parsed.reserved).toBe('');
});
});
describe('Round-trip encoding and parsing', () => {
it('should maintain data integrity through encode -> parse cycle', () => {
const original = validM100;
const encoded = encodeM100(original);
const parsed = parseM100(encoded);
expect(parsed).toEqual(original);
});
it('should handle round-trip with maximum length fields', () => {
const original: M100 = {
code: 'M100',
recordNumber: '999999999',
vatId: '999888777',
universalItemCode: 'UNI' + 'A'.repeat(17), // exactly 20 chars
supplierItemCode: 'SUP' + 'B'.repeat(17), // exactly 20 chars
internalItemCode: 'INT' + 'C'.repeat(17), // exactly 20 chars
itemName: 'Item Name ' + 'D'.repeat(40), // exactly 50 chars
classificationCode: 'CLASS' + 'E'.repeat(5), // exactly 10 chars
classificationDescription: 'Description ' + 'F'.repeat(18), // exactly 30 chars
unitOfMeasure: 'Unit ' + 'G'.repeat(15), // exactly 20 chars (not 16)
openingStock: '999999999.99',
totalStockIn: '888888888.88',
totalStockOut: '777777777.77',
endPeriodCostNonBonded: '99999.99',
endPeriodCostBonded: '88888.88',
reserved: 'Reserved ' + 'M'.repeat(41), // exactly 50 chars
};
const encoded = encodeM100(original);
const parsed = parseM100(encoded);
expect(parsed).toEqual(original);
});
it('should handle round-trip with minimal fields', () => {
const original: M100 = {
code: 'M100',
recordNumber: '1',
vatId: '1',
universalItemCode: '',
supplierItemCode: '',
internalItemCode: 'I',
itemName: 'N',
classificationCode: '',
classificationDescription: '',
unitOfMeasure: '',
openingStock: '0',
totalStockIn: '0',
totalStockOut: '0',
endPeriodCostNonBonded: '',
endPeriodCostBonded: '',
reserved: '',
};
const encoded = encodeM100(original);
const parsed = parseM100(encoded);
expect(parsed).toEqual(original);
});
});
describe('Error handling', () => {
it('should throw validation error for invalid parsed data', () => {
// Create a line that parses but fails schema validation
const invalidLine = 'M100 ' + ' '.repeat(281); // empty recordNumber
expect(() => parseM100(invalidLine)).toThrow();
});
it('should throw error when feeding wrong length line', () => {
const wrongLengthLine = 'M100' + ' '.repeat(100); // 104 chars instead of 294
expect(() => parseM100(wrongLengthLine)).toThrow(
'Invalid M100 record length: expected 294 characters, got 104',
);
});
});
});