@accounter/shaam-uniform-format-generator
Version:
Fully typed application that generates, parses, and validates SHAAM uniform format tax reports (INI.TXT and BKMVDATA.TXT).
257 lines (218 loc) • 9.69 kB
text/typescript
import { describe, expect, it } from 'vitest';
import { SHAAM_VERSION } from '../../src/constants.js';
import { encodeZ900, parseZ900, Z900Schema, type Z900 } from '../../src/generator/records/z900.js';
describe('Z900 Record', () => {
const validZ900: Z900 = {
code: 'Z900',
recordNumber: 123,
vatId: '987654321',
uniqueId: 123_456_789_012_345,
totalRecords: 1000,
reserved: '',
};
describe('Z900Schema validation', () => {
it('should validate a correct Z900 record', () => {
expect(() => Z900Schema.parse(validZ900)).not.toThrow();
});
it('should require code to be exactly "Z900"', () => {
const invalidRecord = { ...validZ900, code: 'Z800' };
expect(() => Z900Schema.parse(invalidRecord)).toThrow();
});
it('should require recordNumber to be non-empty and max 9 characters', () => {
expect(() => Z900Schema.parse({ ...validZ900, recordNumber: 0 })).toThrow();
expect(() => Z900Schema.parse({ ...validZ900, recordNumber: 1_000_000_000 })).toThrow();
expect(() => Z900Schema.parse({ ...validZ900, recordNumber: 123_456_789 })).not.toThrow();
});
it('should require vatId to be non-empty and max 9 characters', () => {
expect(() => Z900Schema.parse({ ...validZ900, vatId: '' })).toThrow();
expect(() => Z900Schema.parse({ ...validZ900, vatId: '1234567890' })).toThrow();
expect(() => Z900Schema.parse({ ...validZ900, vatId: '123456789' })).not.toThrow();
});
it('should require uniqueId to be non-empty and max 15 characters', () => {
expect(() => Z900Schema.parse({ ...validZ900, uniqueId: 0 })).toThrow();
expect(() => Z900Schema.parse({ ...validZ900, uniqueId: 1_000_000_000_000_000 })).toThrow();
expect(() => Z900Schema.parse({ ...validZ900, uniqueId: 123_456_789_012_345 })).not.toThrow();
});
it('should require totalRecords to be non-empty and max 15 characters', () => {
expect(() => Z900Schema.parse({ ...validZ900, totalRecords: 0 })).toThrow();
expect(() =>
Z900Schema.parse({ ...validZ900, totalRecords: 1_000_000_000_000_000 }),
).toThrow();
expect(() =>
Z900Schema.parse({ ...validZ900, totalRecords: 123_456_789_012_345 }),
).not.toThrow();
});
it('should validate reserved field', () => {
expect(() => Z900Schema.parse({ ...validZ900, reserved: '' })).not.toThrow();
expect(() => Z900Schema.parse({ ...validZ900, reserved: 'Reserved data' })).not.toThrow();
expect(() => Z900Schema.parse({ ...validZ900, reserved: 'A'.repeat(50) })).not.toThrow();
expect(() => Z900Schema.parse({ ...validZ900, reserved: 'A'.repeat(51) })).toThrow();
});
it('should set default empty string for reserved field when missing', () => {
const { reserved: _reserved, ...recordWithoutReserved } = validZ900;
const parsed = Z900Schema.parse(recordWithoutReserved);
expect(parsed.reserved).toBe('');
});
});
describe('encodeZ900', () => {
it('should encode a Z900 record to fixed-width format', () => {
const encoded = encodeZ900(validZ900);
// Should end with CRLF
expect(encoded).toMatch(/\r\n$/);
// Remove CRLF for length check
const withoutCRLF = encoded.replace(/\r\n$/, '');
expect(withoutCRLF).toHaveLength(110);
// Check field positions and padding
expect(withoutCRLF.slice(0, 4)).toBe('Z900'); // 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, 37)).toBe('123456789012345'); // uniqueId zero-padded (15)
expect(withoutCRLF.slice(37, 45)).toBe(SHAAM_VERSION); // static SHAAM version (8)
expect(withoutCRLF.slice(45, 60)).toBe('000000000001000'); // totalRecords zero-padded (15)
expect(withoutCRLF.slice(60, 110)).toBe(' '.repeat(50)); // reserved left-aligned (50)
});
it('should handle maximum length fields', () => {
const maxLengthRecord: Z900 = {
code: 'Z900',
recordNumber: 123_456_789, // 9 digits
vatId: '123456789', // 9 chars
uniqueId: 123_456_789_012_345, // 15 digits
totalRecords: 123_456_789_012_345, // 15 digits
reserved: 'A'.repeat(50), // 50 chars
};
const encoded = encodeZ900(maxLengthRecord);
const withoutCRLF = encoded.replace(/\r\n$/, '');
expect(withoutCRLF).toHaveLength(110);
expect(withoutCRLF.slice(0, 4)).toBe('Z900');
expect(withoutCRLF.slice(4, 13)).toBe('123456789');
expect(withoutCRLF.slice(13, 22)).toBe('123456789');
expect(withoutCRLF.slice(22, 37)).toBe('123456789012345');
expect(withoutCRLF.slice(37, 45)).toBe(SHAAM_VERSION);
expect(withoutCRLF.slice(45, 60)).toBe('123456789012345');
expect(withoutCRLF.slice(60, 110)).toBe('A'.repeat(50));
});
it('should truncate fields that exceed maximum length', () => {
const oversizedRecord: Z900 = {
code: 'Z900',
recordNumber: 999_999_999, // max 9 digits
vatId: '1234567890', // 10 chars (max 9)
uniqueId: 999_999_999_999_999, // max 15 digits
totalRecords: 999_999_999_999_999, // max 15 digits
reserved: 'A'.repeat(60), // 60 chars (max 50)
};
const encoded = encodeZ900(oversizedRecord);
const withoutCRLF = encoded.replace(/\r\n$/, '');
expect(withoutCRLF).toHaveLength(110);
expect(withoutCRLF.slice(4, 13)).toBe('999999999'); // max 9 digits
expect(withoutCRLF.slice(13, 22)).toBe('123456789'); // truncated
expect(withoutCRLF.slice(22, 37)).toBe('999999999999999'); // max 15 digits
expect(withoutCRLF.slice(37, 45)).toBe(SHAAM_VERSION); // always the constant
expect(withoutCRLF.slice(45, 60)).toBe('999999999999999'); // max 15 digits
expect(withoutCRLF.slice(60, 110)).toBe('A'.repeat(50)); // truncated
});
});
describe('parseZ900', () => {
it('should parse a valid Z900 record line', () => {
const line =
'Z900000000123987654321123456789012345' +
SHAAM_VERSION +
'000000000001000' +
' '.repeat(50) +
'\r\n';
const parsed = parseZ900(line);
expect(parsed).toEqual({
code: 'Z900',
recordNumber: 123,
vatId: '987654321',
uniqueId: 123_456_789_012_345,
totalRecords: 1000,
reserved: '',
});
});
it('should parse line without CRLF', () => {
const line =
'Z900000000123987654321123456789012345' +
SHAAM_VERSION +
'000000000001000' +
' '.repeat(50);
const parsed = parseZ900(line);
expect(parsed.code).toBe('Z900');
expect(parsed.recordNumber).toBe(123);
});
it('should throw error for invalid line length', () => {
const shortLine = 'Z900' + ' '.repeat(50);
expect(() => parseZ900(shortLine)).toThrow(
'Invalid Z900 record length: expected 110 characters, got 54',
);
const longLine = 'Z900' + ' '.repeat(200);
expect(() => parseZ900(longLine)).toThrow(
'Invalid Z900 record length: expected 110 characters, got 204',
);
});
it('should throw error for invalid record code', () => {
const invalidLine = 'Z800' + ' '.repeat(106) + '\r\n';
expect(() => parseZ900(invalidLine)).toThrow(
'Invalid Z900 record code: expected "Z900", got "Z800"',
);
});
it('should throw error for invalid SHAAM version', () => {
const invalidLine =
'Z900000000123987654321123456789012345INVALID 000000000001000' + ' '.repeat(50) + '\r\n';
expect(() => parseZ900(invalidLine)).toThrow('Invalid SHAAM version');
});
it('should handle fields with trailing spaces correctly', () => {
const line =
'Z900000000123000123456000000000123456' +
SHAAM_VERSION +
'000000000009999' +
' '.repeat(50) +
'\r\n';
const parsed = parseZ900(line);
expect(parsed.vatId).toBe('123456');
expect(parsed.uniqueId).toBe(123_456);
expect(parsed.totalRecords).toBe(9999);
expect(parsed.reserved).toBe('');
});
});
describe('Round-trip encoding and parsing', () => {
it('should maintain data integrity through encode -> parse cycle', () => {
const original = validZ900;
const encoded = encodeZ900(original);
const parsed = parseZ900(encoded);
expect(parsed).toEqual(original);
});
it('should handle round-trip with maximum length fields', () => {
const original: Z900 = {
code: 'Z900',
recordNumber: 999_999_999,
vatId: '999888777',
uniqueId: 999_888_777_666_555,
totalRecords: 999_888_777_666_555,
reserved: 'A'.repeat(50),
};
const encoded = encodeZ900(original);
const parsed = parseZ900(encoded);
expect(parsed).toEqual(original);
});
it('should handle round-trip with minimal fields', () => {
const original: Z900 = {
code: 'Z900',
recordNumber: 1,
vatId: '1',
uniqueId: 1,
totalRecords: 1,
reserved: '',
};
const encoded = encodeZ900(original);
const parsed = parseZ900(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 = 'Z900 ' + ' '.repeat(97); // empty recordNumber
expect(() => parseZ900(invalidLine)).toThrow();
});
});
});