602 lines
19 KiB
TypeScript
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(); |