einvoice/test/suite/einvoice_edge-cases/test.edge-07.max-field-lengths.ts

602 lines
19 KiB
TypeScript

import { tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
tap.test('EDGE-07: Maximum Field Lengths - should handle fields at maximum allowed lengths', async () => {
console.log('Testing maximum field lengths in e-invoices...\n');
// Test 1: Standard field length limits per EN16931
const testStandardFieldLimits = async () => {
const fieldTests = [
{ field: 'invoiceId', maxLength: 30, testValue: 'INV' }, // BT-1 Invoice number
{ field: 'customerName', maxLength: 200, testValue: 'ACME' }, // BT-44 Buyer name
{ field: 'streetName', maxLength: 1000, testValue: 'Street' }, // BT-35 Buyer address line 1
{ field: 'subject', maxLength: 100, testValue: 'SUBJ' }, // Invoice subject
{ field: 'notes', maxLength: 5000, testValue: 'NOTE' } // BT-22 Invoice note
];
console.log('Test 1 - Standard field limits:');
for (const test of fieldTests) {
try {
const einvoice = new EInvoice();
einvoice.issueDate = new Date(2024, 0, 1);
// Test at max length
const maxValue = test.testValue.repeat(Math.ceil(test.maxLength / test.testValue.length)).substring(0, test.maxLength);
if (test.field === 'invoiceId') {
einvoice.invoiceId = maxValue;
} else if (test.field === 'subject') {
einvoice.subject = maxValue;
}
einvoice.from = {
type: 'company',
name: test.field === 'customerName' ? maxValue : 'Test Company',
description: 'Testing max field lengths',
address: {
streetName: test.field === 'streetName' ? maxValue : 'Test Street',
houseNumber: '1',
postalCode: '12345',
city: 'Test City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: 'Commercial Register'
}
};
einvoice.to = {
type: 'company',
name: 'Customer Company',
description: 'Customer',
address: {
streetName: 'Customer Street',
houseNumber: '2',
postalCode: '54321',
city: 'Customer City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2019, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE987654321',
registrationId: 'HRB 54321',
registrationName: 'Commercial Register'
}
};
if (test.field === 'notes') {
einvoice.notes = [maxValue];
}
einvoice.items = [{
position: 1,
name: 'Test Item',
articleNumber: 'TEST-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
// Generate XML
const xmlString = await einvoice.toXmlString('ubl');
// Test round-trip
const newInvoice = new EInvoice();
await newInvoice.fromXmlString(xmlString);
let preserved = false;
if (test.field === 'invoiceId') {
preserved = newInvoice.invoiceId === maxValue;
} else if (test.field === 'customerName') {
preserved = newInvoice.from.name === maxValue;
} else if (test.field === 'streetName') {
preserved = newInvoice.from.address.streetName === maxValue;
} else if (test.field === 'subject') {
preserved = newInvoice.subject === maxValue;
} else if (test.field === 'notes') {
preserved = newInvoice.notes?.[0] === maxValue;
}
console.log(` ${test.field} (${test.maxLength} chars): ${preserved ? 'preserved' : 'truncated'}`);
// Test over max length (+50 chars)
const overValue = test.testValue.repeat(Math.ceil((test.maxLength + 50) / test.testValue.length)).substring(0, test.maxLength + 50);
const overInvoice = new EInvoice();
overInvoice.issueDate = new Date(2024, 0, 1);
if (test.field === 'invoiceId') {
overInvoice.invoiceId = overValue;
} else if (test.field === 'subject') {
overInvoice.subject = overValue;
}
overInvoice.from = {
type: 'company',
name: test.field === 'customerName' ? overValue : 'Test Company',
description: 'Testing over max field lengths',
address: {
streetName: test.field === 'streetName' ? overValue : 'Test Street',
houseNumber: '1',
postalCode: '12345',
city: 'Test City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: 'Commercial Register'
}
};
overInvoice.to = {
type: 'person',
name: 'Test',
surname: 'Customer',
salutation: 'Mr' as const,
sex: 'male' as const,
title: 'Doctor' as const,
description: 'Test customer',
address: {
streetName: 'Customer Street',
houseNumber: '2',
postalCode: '54321',
city: 'Customer City',
country: 'DE'
}
};
if (test.field === 'notes') {
overInvoice.notes = [overValue];
}
overInvoice.items = [{
position: 1,
name: 'Test Item',
articleNumber: 'TEST-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
try {
await overInvoice.toXmlString('ubl');
console.log(` ${test.field} (+50 chars): handled gracefully`);
} catch (error) {
console.log(` ${test.field} (+50 chars): properly rejected`);
}
} catch (error) {
console.log(` ${test.field}: Failed - ${error.message}`);
}
}
};
// Test 2: Unicode character length vs byte length
const testUnicodeLengthVsBytes = async () => {
console.log('\nTest 2 - Unicode length vs bytes:');
const testCases = [
{ name: 'ASCII', char: 'A', bytesPerChar: 1 },
{ name: 'Latin Extended', char: 'ñ', bytesPerChar: 2 },
{ name: 'Chinese', char: '中', bytesPerChar: 3 },
{ name: 'Emoji', char: '😀', bytesPerChar: 4 }
];
const maxChars = 100;
for (const test of testCases) {
try {
const value = test.char.repeat(maxChars);
const byteLength = Buffer.from(value, 'utf8').length;
const einvoice = new EInvoice();
einvoice.issueDate = new Date(2024, 0, 1);
einvoice.invoiceId = 'UNICODE-TEST';
einvoice.from = {
type: 'company',
name: value,
description: `Unicode test: ${test.name}`,
address: {
streetName: 'Unicode Street',
houseNumber: '1',
postalCode: '12345',
city: 'Unicode City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: 'Commercial Register'
}
};
einvoice.to = {
type: 'company',
name: 'Customer Company',
description: 'Customer',
address: {
streetName: 'Customer Street',
houseNumber: '2',
postalCode: '54321',
city: 'Customer City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2019, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE987654321',
registrationId: 'HRB 54321',
registrationName: 'Commercial Register'
}
};
einvoice.items = [{
position: 1,
name: 'Test Item',
articleNumber: 'TEST-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
const xmlString = await einvoice.toXmlString('cii');
const newInvoice = new EInvoice();
await newInvoice.fromXmlString(xmlString);
const retrievedValue = newInvoice.from.name;
const preserved = retrievedValue === value;
console.log(` ${test.name}: chars=${value.length}, bytes=${byteLength}, preserved=${preserved ? 'Yes' : 'No'}`);
} catch (error) {
console.log(` ${test.name}: Failed - ${error.message}`);
}
}
};
// Test 3: Long invoice numbers per EN16931 BT-1
const testLongInvoiceNumbers = async () => {
console.log('\nTest 3 - Long invoice numbers:');
const lengths = [10, 20, 30, 50]; // EN16931 recommends max 30
for (const length of lengths) {
try {
const einvoice = new EInvoice();
einvoice.issueDate = new Date(2024, 0, 1);
einvoice.invoiceId = 'INV-' + '0'.repeat(length - 4);
einvoice.from = {
type: 'company',
name: 'Test Company',
description: 'Testing long invoice numbers',
address: {
streetName: 'Test Street',
houseNumber: '1',
postalCode: '12345',
city: 'Test City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: 'Commercial Register'
}
};
einvoice.to = {
type: 'person',
name: 'Test',
surname: 'Customer',
salutation: 'Mr' as const,
sex: 'male' as const,
title: 'Doctor' as const,
description: 'Test customer',
address: {
streetName: 'Customer Street',
houseNumber: '2',
postalCode: '54321',
city: 'Customer City',
country: 'DE'
}
};
einvoice.items = [{
position: 1,
name: 'Test Item',
articleNumber: 'TEST-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
const xmlString = await einvoice.toXmlString('xrechnung');
const newInvoice = new EInvoice();
await newInvoice.fromXmlString(xmlString);
const preserved = newInvoice.invoiceId.length === length;
const status = length <= 30 ? 'within spec' : 'over spec';
console.log(` Invoice ID ${length} chars: ${preserved ? 'preserved' : 'modified'} (${status})`);
} catch (error) {
console.log(` Invoice ID ${length} chars: Failed - ${error.message}`);
}
}
};
// Test 4: Line item count limits
const testLineItemCountLimits = async () => {
console.log('\nTest 4 - Line item count limits:');
const itemCounts = [10, 50, 100, 500];
for (const count of itemCounts) {
try {
const einvoice = new EInvoice();
einvoice.issueDate = new Date(2024, 0, 1);
einvoice.invoiceId = `MANY-ITEMS-${count}`;
einvoice.from = {
type: 'company',
name: 'Bulk Seller Company',
description: 'Testing many line items',
address: {
streetName: 'Bulk Street',
houseNumber: '1',
postalCode: '12345',
city: 'Bulk City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: 'Commercial Register'
}
};
einvoice.to = {
type: 'company',
name: 'Bulk Buyer Company',
description: 'Customer buying many items',
address: {
streetName: 'Buyer Street',
houseNumber: '2',
postalCode: '54321',
city: 'Buyer City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2019, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE987654321',
registrationId: 'HRB 54321',
registrationName: 'Commercial Register'
}
};
// Create many items
einvoice.items = [];
for (let i = 0; i < count; i++) {
einvoice.items.push({
position: i + 1,
name: `Item ${i + 1}`,
articleNumber: `ART-${String(i + 1).padStart(5, '0')}`,
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 10 + (i % 100),
vatPercentage: 19
});
}
const xmlString = await einvoice.toXmlString('ubl');
const newInvoice = new EInvoice();
await newInvoice.fromXmlString(xmlString);
const itemsParsed = newInvoice.items.length;
console.log(` Line items ${count}: parsed=${itemsParsed}, preserved=${itemsParsed === count ? 'Yes' : 'No'}`);
} catch (error) {
console.log(` Line items ${count}: Failed - ${error.message}`);
}
}
};
// Test 5: Long email addresses per RFC 5321
const testLongEmailAddresses = async () => {
console.log('\nTest 5 - Long email addresses:');
const emailLengths = [50, 100, 254]; // RFC 5321 limit is 254
for (const length of emailLengths) {
try {
const localPart = 'x'.repeat(Math.max(1, length - 20));
const email = localPart + '@example.com';
const finalEmail = email.substring(0, length);
const einvoice = new EInvoice();
einvoice.issueDate = new Date(2024, 0, 1);
einvoice.invoiceId = 'EMAIL-TEST';
einvoice.electronicAddress = {
scheme: 'EMAIL',
value: finalEmail
};
einvoice.from = {
type: 'company',
name: 'Email Test Company',
description: 'Testing long email addresses',
address: {
streetName: 'Email Street',
houseNumber: '1',
postalCode: '12345',
city: 'Email City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: 'Commercial Register'
}
};
einvoice.to = {
type: 'company',
name: 'Customer Company',
description: 'Customer',
address: {
streetName: 'Customer Street',
houseNumber: '2',
postalCode: '54321',
city: 'Customer City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2019, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE987654321',
registrationId: 'HRB 54321',
registrationName: 'Commercial Register'
}
};
einvoice.items = [{
position: 1,
name: 'Test Item',
articleNumber: 'TEST-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
const xmlString = await einvoice.toXmlString('ubl');
const newInvoice = new EInvoice();
await newInvoice.fromXmlString(xmlString);
const preserved = newInvoice.electronicAddress?.value === finalEmail;
const status = length <= 254 ? 'within RFC' : 'over RFC';
console.log(` Email ${length} chars: ${preserved ? 'preserved' : 'modified'} (${status})`);
} catch (error) {
console.log(` Email ${length} chars: Failed - ${error.message}`);
}
}
};
// Test 6: Decimal precision limits
const testDecimalPrecisionLimits = async () => {
console.log('\nTest 6 - Decimal precision limits:');
const precisionTests = [
{ decimals: 2, value: 123456789.12, description: 'Standard 2 decimals' },
{ decimals: 4, value: 123456.1234, description: 'High precision 4 decimals' },
{ decimals: 6, value: 123.123456, description: 'Very high precision 6 decimals' }
];
for (const test of precisionTests) {
try {
const einvoice = new EInvoice();
einvoice.issueDate = new Date(2024, 0, 1);
einvoice.invoiceId = 'DECIMAL-TEST';
einvoice.from = {
type: 'company',
name: 'Decimal Test Company',
description: 'Testing decimal precision',
address: {
streetName: 'Decimal Street',
houseNumber: '1',
postalCode: '12345',
city: 'Decimal City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: 'Commercial Register'
}
};
einvoice.to = {
type: 'person',
name: 'Test',
surname: 'Customer',
salutation: 'Mr' as const,
sex: 'male' as const,
title: 'Doctor' as const,
description: 'Test customer',
address: {
streetName: 'Customer Street',
houseNumber: '2',
postalCode: '54321',
city: 'Customer City',
country: 'DE'
}
};
einvoice.items = [{
position: 1,
name: 'High Precision Item',
articleNumber: 'DECIMAL-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: test.value,
vatPercentage: 19
}];
const xmlString = await einvoice.toXmlString('cii');
const newInvoice = new EInvoice();
await newInvoice.fromXmlString(xmlString);
const parsedValue = newInvoice.items[0].unitNetPrice;
const preserved = Math.abs(parsedValue - test.value) < 0.000001;
console.log(` ${test.description}: original=${test.value}, parsed=${parsedValue}, preserved=${preserved ? 'Yes' : 'No'}`);
} catch (error) {
console.log(` ${test.description}: Failed - ${error.message}`);
}
}
};
// Run all tests
await testStandardFieldLimits();
await testUnicodeLengthVsBytes();
await testLongInvoiceNumbers();
await testLineItemCountLimits();
await testLongEmailAddresses();
await testDecimalPrecisionLimits();
console.log('\n=== Maximum Field Lengths Test Summary ===');
console.log('Standard field limits: Tested');
console.log('Unicode handling: Tested');
console.log('Long invoice numbers: Tested');
console.log('Line item limits: Tested');
console.log('Email address limits: Tested');
console.log('Decimal precision: Tested');
});
tap.start();