update
This commit is contained in:
729
test/suite/einvoice_edge-cases/test.edge-07.max-field-lengths.ts
Normal file
729
test/suite/einvoice_edge-cases/test.edge-07.max-field-lengths.ts
Normal file
@ -0,0 +1,729 @@
|
||||
import { tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { EInvoice } from '../../../ts/index.js';
|
||||
import { PerformanceTracker } from '../performance.tracker.js';
|
||||
|
||||
const performanceTracker = new PerformanceTracker('EDGE-07: Maximum Field Lengths');
|
||||
|
||||
tap.test('EDGE-07: Maximum Field Lengths - should handle fields at maximum allowed lengths', async (t) => {
|
||||
const einvoice = new EInvoice();
|
||||
|
||||
// Test 1: Standard field length limits
|
||||
const standardFieldLimits = await performanceTracker.measureAsync(
|
||||
'standard-field-limits',
|
||||
async () => {
|
||||
const fieldTests = [
|
||||
{ field: 'InvoiceID', maxLength: 200, standard: 'EN16931' },
|
||||
{ field: 'CustomerName', maxLength: 200, standard: 'EN16931' },
|
||||
{ field: 'Description', maxLength: 1000, standard: 'EN16931' },
|
||||
{ field: 'Note', maxLength: 5000, standard: 'EN16931' },
|
||||
{ field: 'Reference', maxLength: 200, standard: 'EN16931' },
|
||||
{ field: 'Email', maxLength: 254, standard: 'RFC5321' },
|
||||
{ field: 'Phone', maxLength: 30, standard: 'ITU-T' },
|
||||
{ field: 'PostalCode', maxLength: 20, standard: 'UPU' }
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const test of fieldTests) {
|
||||
// Test at max length
|
||||
const maxValue = 'X'.repeat(test.maxLength);
|
||||
const xml = createInvoiceWithField(test.field, maxValue);
|
||||
|
||||
try {
|
||||
const parsed = await einvoice.parseXML(xml);
|
||||
const validated = await einvoice.validate(parsed);
|
||||
|
||||
results.push({
|
||||
field: test.field,
|
||||
length: test.maxLength,
|
||||
parsed: true,
|
||||
valid: validated?.isValid || false,
|
||||
preserved: getFieldValue(parsed, test.field)?.length === test.maxLength
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
field: test.field,
|
||||
length: test.maxLength,
|
||||
parsed: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
|
||||
// Test over max length
|
||||
const overValue = 'X'.repeat(test.maxLength + 1);
|
||||
const overXml = createInvoiceWithField(test.field, overValue);
|
||||
|
||||
try {
|
||||
const parsed = await einvoice.parseXML(overXml);
|
||||
const validated = await einvoice.validate(parsed);
|
||||
|
||||
results.push({
|
||||
field: test.field,
|
||||
length: test.maxLength + 1,
|
||||
parsed: true,
|
||||
valid: validated?.isValid || false,
|
||||
truncated: getFieldValue(parsed, test.field)?.length <= test.maxLength
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
field: test.field,
|
||||
length: test.maxLength + 1,
|
||||
parsed: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
standardFieldLimits.forEach(result => {
|
||||
if (result.length <= result.maxLength) {
|
||||
t.ok(result.valid, `Field ${result.field} at max length should be valid`);
|
||||
} else {
|
||||
t.notOk(result.valid, `Field ${result.field} over max length should be invalid`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 2: Unicode character length vs byte length
|
||||
const unicodeLengthTests = await performanceTracker.measureAsync(
|
||||
'unicode-length-vs-bytes',
|
||||
async () => {
|
||||
const testCases = [
|
||||
{
|
||||
name: 'ascii-only',
|
||||
char: 'A',
|
||||
bytesPerChar: 1
|
||||
},
|
||||
{
|
||||
name: 'latin-extended',
|
||||
char: 'ñ',
|
||||
bytesPerChar: 2
|
||||
},
|
||||
{
|
||||
name: 'chinese',
|
||||
char: '中',
|
||||
bytesPerChar: 3
|
||||
},
|
||||
{
|
||||
name: 'emoji',
|
||||
char: '😀',
|
||||
bytesPerChar: 4
|
||||
}
|
||||
];
|
||||
|
||||
const results = [];
|
||||
const maxChars = 100;
|
||||
|
||||
for (const test of testCases) {
|
||||
const value = test.char.repeat(maxChars);
|
||||
const byteLength = Buffer.from(value, 'utf8').length;
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<ID>TEST</ID>
|
||||
<CustomerName>${value}</CustomerName>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const parsed = await einvoice.parseXML(xml);
|
||||
const retrievedValue = parsed?.CustomerName || '';
|
||||
|
||||
results.push({
|
||||
type: test.name,
|
||||
charCount: value.length,
|
||||
byteCount: byteLength,
|
||||
expectedBytes: maxChars * test.bytesPerChar,
|
||||
preserved: retrievedValue === value,
|
||||
retrievedLength: retrievedValue.length,
|
||||
retrievedBytes: Buffer.from(retrievedValue, 'utf8').length
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
type: test.name,
|
||||
charCount: value.length,
|
||||
byteCount: byteLength,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
unicodeLengthTests.forEach(result => {
|
||||
t.ok(result.preserved || result.error,
|
||||
`Unicode ${result.type} field should be handled correctly`);
|
||||
if (result.preserved) {
|
||||
t.equal(result.retrievedLength, result.charCount,
|
||||
`Character count should be preserved for ${result.type}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 3: Format-specific field limits
|
||||
const formatSpecificLimits = await performanceTracker.measureAsync(
|
||||
'format-specific-limits',
|
||||
async () => {
|
||||
const formatLimits = [
|
||||
{
|
||||
format: 'ubl',
|
||||
fields: [
|
||||
{ name: 'ID', maxLength: 200 },
|
||||
{ name: 'Note', maxLength: 1000 },
|
||||
{ name: 'DocumentCurrencyCode', maxLength: 3 }
|
||||
]
|
||||
},
|
||||
{
|
||||
format: 'cii',
|
||||
fields: [
|
||||
{ name: 'ID', maxLength: 35 },
|
||||
{ name: 'Content', maxLength: 5000 },
|
||||
{ name: 'TypeCode', maxLength: 4 }
|
||||
]
|
||||
},
|
||||
{
|
||||
format: 'xrechnung',
|
||||
fields: [
|
||||
{ name: 'BT-1', maxLength: 16 }, // Invoice number
|
||||
{ name: 'BT-22', maxLength: 1000 }, // Note
|
||||
{ name: 'BT-5', maxLength: 3 } // Currency
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const format of formatLimits) {
|
||||
for (const field of format.fields) {
|
||||
const value = 'A'.repeat(field.maxLength);
|
||||
const invoice = createFormatSpecificInvoice(format.format, field.name, value);
|
||||
|
||||
try {
|
||||
const parsed = await einvoice.parseDocument(invoice);
|
||||
const validated = await einvoice.validateFormat(parsed, format.format);
|
||||
|
||||
results.push({
|
||||
format: format.format,
|
||||
field: field.name,
|
||||
maxLength: field.maxLength,
|
||||
valid: validated?.isValid || false,
|
||||
compliant: validated?.formatCompliant || false
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
format: format.format,
|
||||
field: field.name,
|
||||
maxLength: field.maxLength,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
formatSpecificLimits.forEach(result => {
|
||||
t.ok(result.valid || result.error,
|
||||
`${result.format} field ${result.field} at max length was processed`);
|
||||
});
|
||||
|
||||
// Test 4: Extreme length edge cases
|
||||
const extremeLengthCases = await performanceTracker.measureAsync(
|
||||
'extreme-length-edge-cases',
|
||||
async () => {
|
||||
const extremeCases = [
|
||||
{ length: 0, name: 'empty' },
|
||||
{ length: 1, name: 'single-char' },
|
||||
{ length: 255, name: 'common-db-limit' },
|
||||
{ length: 65535, name: 'uint16-max' },
|
||||
{ length: 1000000, name: 'one-million' },
|
||||
{ length: 10000000, name: 'ten-million' }
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const testCase of extremeCases) {
|
||||
const value = testCase.length > 0 ? 'X'.repeat(testCase.length) : '';
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<ID>EXTREME-${testCase.name}</ID>
|
||||
<LongField>${value}</LongField>
|
||||
</Invoice>`;
|
||||
|
||||
const startTime = Date.now();
|
||||
const startMemory = process.memoryUsage();
|
||||
|
||||
try {
|
||||
const parsed = await einvoice.parseXML(xml);
|
||||
|
||||
const endTime = Date.now();
|
||||
const endMemory = process.memoryUsage();
|
||||
|
||||
results.push({
|
||||
length: testCase.length,
|
||||
name: testCase.name,
|
||||
parsed: true,
|
||||
timeTaken: endTime - startTime,
|
||||
memoryUsed: endMemory.heapUsed - startMemory.heapUsed,
|
||||
fieldPreserved: parsed?.LongField?.length === testCase.length
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
length: testCase.length,
|
||||
name: testCase.name,
|
||||
parsed: false,
|
||||
error: error.message,
|
||||
isLengthError: error.message.includes('length') || error.message.includes('size')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
extremeLengthCases.forEach(result => {
|
||||
if (result.length <= 65535) {
|
||||
t.ok(result.parsed, `Length ${result.name} should be handled`);
|
||||
} else {
|
||||
t.ok(!result.parsed || result.isLengthError,
|
||||
`Extreme length ${result.name} should be limited`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 5: Line item count limits
|
||||
const lineItemCountLimits = await performanceTracker.measureAsync(
|
||||
'line-item-count-limits',
|
||||
async () => {
|
||||
const itemCounts = [100, 1000, 9999, 10000, 99999];
|
||||
const results = [];
|
||||
|
||||
for (const count of itemCounts) {
|
||||
const invoice = createInvoiceWithManyItems(count);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const parsed = await einvoice.parseXML(invoice);
|
||||
const itemsParsed = countItems(parsed);
|
||||
|
||||
const endTime = Date.now();
|
||||
|
||||
results.push({
|
||||
requestedCount: count,
|
||||
parsedCount: itemsParsed,
|
||||
success: true,
|
||||
timeTaken: endTime - startTime,
|
||||
avgTimePerItem: (endTime - startTime) / count
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
requestedCount: count,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
lineItemCountLimits.forEach(result => {
|
||||
if (result.requestedCount <= 10000) {
|
||||
t.ok(result.success, `${result.requestedCount} line items should be supported`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 6: Attachment size limits
|
||||
const attachmentSizeLimits = await performanceTracker.measureAsync(
|
||||
'attachment-size-limits',
|
||||
async () => {
|
||||
const sizes = [
|
||||
{ size: 1024 * 1024, name: '1MB' },
|
||||
{ size: 10 * 1024 * 1024, name: '10MB' },
|
||||
{ size: 50 * 1024 * 1024, name: '50MB' },
|
||||
{ size: 100 * 1024 * 1024, name: '100MB' }
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const test of sizes) {
|
||||
const attachmentData = Buffer.alloc(test.size, 'A');
|
||||
const base64Data = attachmentData.toString('base64');
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<ID>ATT-TEST</ID>
|
||||
<Attachment>
|
||||
<EmbeddedDocumentBinaryObject mimeCode="application/pdf">
|
||||
${base64Data}
|
||||
</EmbeddedDocumentBinaryObject>
|
||||
</Attachment>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const parsed = await einvoice.parseXML(xml);
|
||||
const attachment = extractAttachment(parsed);
|
||||
|
||||
results.push({
|
||||
size: test.name,
|
||||
bytes: test.size,
|
||||
parsed: true,
|
||||
attachmentPreserved: attachment?.length === test.size
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
size: test.name,
|
||||
bytes: test.size,
|
||||
parsed: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
attachmentSizeLimits.forEach(result => {
|
||||
if (result.bytes <= 50 * 1024 * 1024) {
|
||||
t.ok(result.parsed, `Attachment size ${result.size} should be supported`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 7: Decimal precision limits
|
||||
const decimalPrecisionLimits = await performanceTracker.measureAsync(
|
||||
'decimal-precision-limits',
|
||||
async () => {
|
||||
const precisionTests = [
|
||||
{ decimals: 2, value: '12345678901234567890.12' },
|
||||
{ decimals: 4, value: '123456789012345678.1234' },
|
||||
{ decimals: 6, value: '1234567890123456.123456' },
|
||||
{ decimals: 10, value: '123456789012.1234567890' },
|
||||
{ decimals: 20, value: '12.12345678901234567890' },
|
||||
{ decimals: 30, value: '1.123456789012345678901234567890' }
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const test of precisionTests) {
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<TotalAmount currencyID="EUR">${test.value}</TotalAmount>
|
||||
<Items>
|
||||
<Item>
|
||||
<Price>${test.value}</Price>
|
||||
<Quantity>1</Quantity>
|
||||
</Item>
|
||||
</Items>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const parsed = await einvoice.parseXML(xml);
|
||||
const amount = parsed?.TotalAmount;
|
||||
|
||||
// Check precision preservation
|
||||
const preserved = amount?.toString() === test.value;
|
||||
const rounded = amount?.toString() !== test.value;
|
||||
|
||||
results.push({
|
||||
decimals: test.decimals,
|
||||
originalValue: test.value,
|
||||
parsedValue: amount?.toString(),
|
||||
preserved,
|
||||
rounded
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
decimals: test.decimals,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
decimalPrecisionLimits.forEach(result => {
|
||||
t.ok(result.preserved || result.rounded,
|
||||
`Decimal precision ${result.decimals} should be handled`);
|
||||
});
|
||||
|
||||
// Test 8: Maximum nesting with field lengths
|
||||
const nestingWithLengths = await performanceTracker.measureAsync(
|
||||
'nesting-with-field-lengths',
|
||||
async () => {
|
||||
const createDeepStructure = (depth: number, fieldLength: number) => {
|
||||
let xml = '';
|
||||
const fieldValue = 'X'.repeat(fieldLength);
|
||||
|
||||
for (let i = 0; i < depth; i++) {
|
||||
xml += `<Level${i}><Field${i}>${fieldValue}</Field${i}>`;
|
||||
}
|
||||
|
||||
xml += '<Core>Data</Core>';
|
||||
|
||||
for (let i = depth - 1; i >= 0; i--) {
|
||||
xml += `</Level${i}>`;
|
||||
}
|
||||
|
||||
return xml;
|
||||
};
|
||||
|
||||
const tests = [
|
||||
{ depth: 10, fieldLength: 1000 },
|
||||
{ depth: 50, fieldLength: 100 },
|
||||
{ depth: 100, fieldLength: 10 },
|
||||
{ depth: 5, fieldLength: 10000 }
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const test of tests) {
|
||||
const content = createDeepStructure(test.depth, test.fieldLength);
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>${content}</Invoice>`;
|
||||
|
||||
const totalDataSize = test.depth * test.fieldLength;
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const parsed = await einvoice.parseXML(xml);
|
||||
const endTime = Date.now();
|
||||
|
||||
results.push({
|
||||
depth: test.depth,
|
||||
fieldLength: test.fieldLength,
|
||||
totalDataSize,
|
||||
parsed: true,
|
||||
timeTaken: endTime - startTime
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
depth: test.depth,
|
||||
fieldLength: test.fieldLength,
|
||||
totalDataSize,
|
||||
parsed: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
nestingWithLengths.forEach(result => {
|
||||
t.ok(result.parsed || result.error,
|
||||
`Nested structure with depth ${result.depth} and field length ${result.fieldLength} was processed`);
|
||||
});
|
||||
|
||||
// Test 9: Field truncation behavior
|
||||
const fieldTruncationBehavior = await performanceTracker.measureAsync(
|
||||
'field-truncation-behavior',
|
||||
async () => {
|
||||
const truncationTests = [
|
||||
{
|
||||
field: 'ID',
|
||||
maxLength: 50,
|
||||
testValue: 'A'.repeat(100),
|
||||
truncationType: 'hard'
|
||||
},
|
||||
{
|
||||
field: 'Note',
|
||||
maxLength: 1000,
|
||||
testValue: 'B'.repeat(2000),
|
||||
truncationType: 'soft'
|
||||
},
|
||||
{
|
||||
field: 'Email',
|
||||
maxLength: 254,
|
||||
testValue: 'x'.repeat(250) + '@test.com',
|
||||
truncationType: 'smart'
|
||||
}
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const test of truncationTests) {
|
||||
const xml = createInvoiceWithField(test.field, test.testValue);
|
||||
|
||||
try {
|
||||
const parsed = await einvoice.parseXML(xml, {
|
||||
truncateFields: true,
|
||||
truncationMode: test.truncationType
|
||||
});
|
||||
|
||||
const fieldValue = getFieldValue(parsed, test.field);
|
||||
|
||||
results.push({
|
||||
field: test.field,
|
||||
originalLength: test.testValue.length,
|
||||
truncatedLength: fieldValue?.length || 0,
|
||||
truncated: fieldValue?.length < test.testValue.length,
|
||||
withinLimit: fieldValue?.length <= test.maxLength,
|
||||
truncationType: test.truncationType
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
field: test.field,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
fieldTruncationBehavior.forEach(result => {
|
||||
if (result.truncated) {
|
||||
t.ok(result.withinLimit,
|
||||
`Field ${result.field} should be truncated to within limit`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 10: Performance impact of field lengths
|
||||
const performanceImpact = await performanceTracker.measureAsync(
|
||||
'field-length-performance-impact',
|
||||
async () => {
|
||||
const lengths = [10, 100, 1000, 10000, 100000];
|
||||
const results = [];
|
||||
|
||||
for (const length of lengths) {
|
||||
const iterations = 10;
|
||||
const times = [];
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const value = 'X'.repeat(length);
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<ID>PERF-TEST</ID>
|
||||
<Description>${value}</Description>
|
||||
<Note>${value}</Note>
|
||||
<CustomerName>${value}</CustomerName>
|
||||
</Invoice>`;
|
||||
|
||||
const startTime = process.hrtime.bigint();
|
||||
|
||||
try {
|
||||
await einvoice.parseXML(xml);
|
||||
} catch (error) {
|
||||
// Ignore errors for performance testing
|
||||
}
|
||||
|
||||
const endTime = process.hrtime.bigint();
|
||||
times.push(Number(endTime - startTime) / 1000000); // Convert to ms
|
||||
}
|
||||
|
||||
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
||||
|
||||
results.push({
|
||||
fieldLength: length,
|
||||
avgParseTime: avgTime,
|
||||
timePerKB: avgTime / (length * 3 / 1024) // 3 fields
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
// Verify performance doesn't degrade exponentially
|
||||
const timeRatios = performanceImpact.map((r, i) =>
|
||||
i > 0 ? r.avgParseTime / performanceImpact[i-1].avgParseTime : 1
|
||||
);
|
||||
|
||||
timeRatios.forEach((ratio, i) => {
|
||||
if (i > 0) {
|
||||
t.ok(ratio < 15, `Performance scaling should be reasonable at length ${performanceImpact[i].fieldLength}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Print performance summary
|
||||
performanceTracker.printSummary();
|
||||
});
|
||||
|
||||
// Helper function to create invoice with specific field
|
||||
function createInvoiceWithField(field: string, value: string): string {
|
||||
const fieldMap = {
|
||||
'InvoiceID': `<ID>${value}</ID>`,
|
||||
'CustomerName': `<CustomerName>${value}</CustomerName>`,
|
||||
'Description': `<Description>${value}</Description>`,
|
||||
'Note': `<Note>${value}</Note>`,
|
||||
'Reference': `<Reference>${value}</Reference>`,
|
||||
'Email': `<Email>${value}</Email>`,
|
||||
'Phone': `<Phone>${value}</Phone>`,
|
||||
'PostalCode': `<PostalCode>${value}</PostalCode>`
|
||||
};
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<ID>TEST-001</ID>
|
||||
${fieldMap[field] || `<${field}>${value}</${field}>`}
|
||||
</Invoice>`;
|
||||
}
|
||||
|
||||
// Helper function to get field value from parsed object
|
||||
function getFieldValue(parsed: any, field: string): string | undefined {
|
||||
return parsed?.[field] || parsed?.Invoice?.[field];
|
||||
}
|
||||
|
||||
// Helper function to create format-specific invoice
|
||||
function createFormatSpecificInvoice(format: string, field: string, value: string): string {
|
||||
if (format === 'ubl') {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
||||
<${field}>${value}</${field}>
|
||||
</Invoice>`;
|
||||
} else if (format === 'cii') {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
|
||||
<rsm:${field}>${value}</rsm:${field}>
|
||||
</rsm:CrossIndustryInvoice>`;
|
||||
}
|
||||
return createInvoiceWithField(field, value);
|
||||
}
|
||||
|
||||
// Helper function to create invoice with many items
|
||||
function createInvoiceWithManyItems(count: number): string {
|
||||
let items = '';
|
||||
for (let i = 0; i < count; i++) {
|
||||
items += `<Item><ID>${i}</ID><Price>10.00</Price></Item>`;
|
||||
}
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<ID>MANY-ITEMS</ID>
|
||||
<Items>${items}</Items>
|
||||
</Invoice>`;
|
||||
}
|
||||
|
||||
// Helper function to count items
|
||||
function countItems(parsed: any): number {
|
||||
if (!parsed?.Items) return 0;
|
||||
if (Array.isArray(parsed.Items)) return parsed.Items.length;
|
||||
if (parsed.Items.Item) {
|
||||
return Array.isArray(parsed.Items.Item) ? parsed.Items.Item.length : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Helper function to extract attachment
|
||||
function extractAttachment(parsed: any): Buffer | null {
|
||||
const base64Data = parsed?.Attachment?.EmbeddedDocumentBinaryObject;
|
||||
if (base64Data) {
|
||||
return Buffer.from(base64Data, 'base64');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Run the test
|
||||
tap.start();
|
Reference in New Issue
Block a user