einvoice/test/suite/einvoice_validation/test.val-05.calculation-validation.ts

441 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, tap } from '@git.zone/tstest/tapbundle';
import { promises as fs } from 'fs';
import * as path from 'path';
import { CorpusLoader } from '../../helpers/corpus.loader.js';
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
tap.test('VAL-05: Calculation Validation - should validate invoice calculations and totals', async () => {
// Get XML-Rechnung test files which contain various calculation scenarios
const ublFiles = await CorpusLoader.getFiles('UBL_XMLRECHNUNG');
const ciiFiles = await CorpusLoader.getFiles('CII_XMLRECHNUNG');
const coFiles = [...ublFiles, ...ciiFiles].filter(f => f.endsWith('.xml')).slice(0, 10);
console.log(`Testing calculation validation on ${coFiles.length} invoice files`);
const { EInvoice } = await import('../../../ts/index.js');
let validCalculations = 0;
let invalidCalculations = 0;
let errorCount = 0;
const calculationErrors: { file: string; errors: string[] }[] = [];
for (const filePath of coFiles.slice(0, 10)) { // Test first 10 calculation files
const fileName = path.basename(filePath);
try {
const xmlContent = await fs.readFile(filePath, 'utf-8');
const { result: einvoice } = await PerformanceTracker.track(
'calculation-xml-loading',
async () => await EInvoice.fromXml(xmlContent)
);
const { result: validation } = await PerformanceTracker.track(
'calculation-validation',
async () => {
return await einvoice.validate(/* ValidationLevel.BUSINESS */);
},
{ file: fileName }
);
// These are valid files - calculations should be correct
if (validation.valid) {
validCalculations++;
console.log(`${fileName}: Calculations are valid`);
} else if (validation.errors) {
const calcErrors = validation.errors.filter(e =>
e.code && (
e.code.includes('BR-CO') ||
e.message && (
e.message.toLowerCase().includes('calculation') ||
e.message.toLowerCase().includes('sum') ||
e.message.toLowerCase().includes('total') ||
e.message.toLowerCase().includes('amount')
)
)
);
if (calcErrors.length > 0) {
invalidCalculations++;
console.log(`${fileName}: Calculation errors found (${calcErrors.length})`);
calculationErrors.push({
file: fileName,
errors: calcErrors.map(e => `${e.code}: ${e.message}`)
});
} else {
invalidCalculations++;
console.log(`${fileName}: Invalid but no calculation-specific errors found`);
}
}
} catch (error) {
errorCount++;
console.log(`${fileName}: Error - ${error.message}`);
}
}
console.log('\n=== CALCULATION VALIDATION SUMMARY ===');
console.log(`Files with valid calculations: ${validCalculations}`);
console.log(`Files with calculation errors: ${invalidCalculations}`);
console.log(`Processing errors: ${errorCount}`);
// Show sample calculation errors
if (calculationErrors.length > 0) {
console.log('\nSample calculation errors detected:');
calculationErrors.slice(0, 3).forEach(item => {
console.log(` ${item.file}:`);
item.errors.slice(0, 2).forEach(error => {
console.log(` - ${error}`);
});
});
}
// Performance summary
const perfSummary = await PerformanceTracker.getSummary('calculation-validation');
if (perfSummary) {
console.log(`\nCalculation Validation Performance:`);
console.log(` Average: ${perfSummary.average.toFixed(2)}ms`);
console.log(` P95: ${perfSummary.p95.toFixed(2)}ms`);
}
// Expect some calculation validation to work
expect(validCalculations + invalidCalculations).toBeGreaterThan(0);
});
tap.test('VAL-05: Line Item Calculation Validation - should validate individual line calculations', async () => {
const { EInvoice } = await import('../../../ts/index.js');
const lineCalculationTests = [
{
name: 'Correct line calculation',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>LINE-CALC-001</cbc:ID>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">5</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">500.00</cbc:LineExtensionAmount>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>`,
shouldBeValid: true,
description: '5 × 100.00 = 500.00 (correct)'
},
{
name: 'Incorrect line calculation',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>LINE-CALC-002</cbc:ID>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">5</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">600.00</cbc:LineExtensionAmount>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>`,
shouldBeValid: false,
description: '5 × 100.00 ≠ 600.00 (incorrect)'
},
{
name: 'Multiple line items with calculations',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>LINE-CALC-003</cbc:ID>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">2</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">200.00</cbc:LineExtensionAmount>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>2</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">3</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">150.00</cbc:LineExtensionAmount>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">50.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>`,
shouldBeValid: true,
description: 'Line 1: 2×100=200, Line 2: 3×50=150 (both correct)'
}
];
for (const test of lineCalculationTests) {
try {
const { result: validation } = await PerformanceTracker.track(
'line-calculation-test',
async () => {
const einvoice = await EInvoice.fromXml(test.xml);
return await einvoice.validate();
}
);
console.log(`${test.name}: ${validation.valid ? 'VALID' : 'INVALID'}`);
console.log(` ${test.description}`);
if (!test.shouldBeValid && !validation.valid) {
console.log(` ✓ Correctly detected calculation error`);
if (validation.errors) {
const calcErrors = validation.errors.filter(e =>
e.message && e.message.toLowerCase().includes('calculation')
);
console.log(` Calculation errors: ${calcErrors.length}`);
}
} else if (test.shouldBeValid && validation.valid) {
console.log(` ✓ Correctly validated calculation`);
} else {
console.log(` ○ Unexpected result (calculation validation may need implementation)`);
}
} catch (error) {
console.log(`${test.name}: Error - ${error.message}`);
}
}
});
tap.test('VAL-05: Tax Calculation Validation - should validate VAT and tax calculations', async () => {
const { EInvoice } = await import('../../../ts/index.js');
const taxCalculationTests = [
{
name: 'Correct VAT calculation',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>TAX-001</cbc:ID>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">190.00</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">1000.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">190.00</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19</cbc:Percent>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:TaxExclusiveAmount currencyID="EUR">1000.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">1190.00</cbc:TaxInclusiveAmount>
</cac:LegalMonetaryTotal>
</Invoice>`,
shouldBeValid: true,
description: '1000.00 × 19% = 190.00, Total: 1190.00 (correct)'
},
{
name: 'Incorrect VAT calculation',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>TAX-002</cbc:ID>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">200.00</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">1000.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">200.00</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19</cbc:Percent>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:TaxExclusiveAmount currencyID="EUR">1000.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">1200.00</cbc:TaxInclusiveAmount>
</cac:LegalMonetaryTotal>
</Invoice>`,
shouldBeValid: false,
description: '1000.00 × 19% = 190.00, not 200.00 (incorrect)'
}
];
for (const test of taxCalculationTests) {
try {
const { result: validation } = await PerformanceTracker.track(
'tax-calculation-test',
async () => {
const einvoice = await EInvoice.fromXml(test.xml);
return await einvoice.validate();
}
);
console.log(`${test.name}: ${validation.valid ? 'VALID' : 'INVALID'}`);
console.log(` ${test.description}`);
if (!test.shouldBeValid && !validation.valid) {
console.log(` ✓ Correctly detected tax calculation error`);
if (validation.errors) {
const taxErrors = validation.errors.filter(e =>
e.message && (
e.message.toLowerCase().includes('tax') ||
e.message.toLowerCase().includes('vat') ||
e.message.toLowerCase().includes('calculation')
)
);
console.log(` Tax calculation errors: ${taxErrors.length}`);
}
} else if (test.shouldBeValid && validation.valid) {
console.log(` ✓ Correctly validated tax calculation`);
} else {
console.log(` ○ Unexpected result (tax calculation validation may need implementation)`);
}
} catch (error) {
console.log(`${test.name}: Error - ${error.message}`);
}
}
});
tap.test('VAL-05: Rounding and Precision Validation - should handle rounding correctly', async () => {
const { EInvoice } = await import('../../../ts/index.js');
const roundingTests = [
{
name: 'Proper rounding to 2 decimal places',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>ROUND-001</cbc:ID>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">3</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">10.00</cbc:LineExtensionAmount>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">3.33</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>`,
description: '3 × 3.33 = 9.99 ≈ 10.00 (acceptable rounding)'
},
{
name: 'Excessive precision',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>ROUND-002</cbc:ID>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">1</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">10.123456789</cbc:LineExtensionAmount>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">10.123456789</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>`,
description: 'Amounts with excessive decimal precision'
}
];
for (const test of roundingTests) {
try {
const { result: validation } = await PerformanceTracker.track(
'rounding-validation-test',
async () => {
const einvoice = await EInvoice.fromXml(test.xml);
return await einvoice.validate();
}
);
console.log(`${test.name}: ${validation.valid ? 'VALID' : 'INVALID'}`);
console.log(` ${test.description}`);
if (!validation.valid && validation.errors) {
const roundingErrors = validation.errors.filter(e =>
e.message && (
e.message.toLowerCase().includes('rounding') ||
e.message.toLowerCase().includes('precision') ||
e.message.toLowerCase().includes('decimal')
)
);
console.log(` Rounding/precision errors: ${roundingErrors.length}`);
} else {
console.log(` No rounding/precision issues detected`);
}
} catch (error) {
console.log(`${test.name}: Error - ${error.message}`);
}
}
});
tap.test('VAL-05: Complex Calculation Scenarios - should handle complex invoice calculations', async () => {
const { EInvoice } = await import('../../../ts/index.js');
// Test with a complex invoice involving discounts, allowances, and charges
const complexCalculationXml = `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>COMPLEX-CALC</cbc:ID>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">10</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">900.00</cbc:LineExtensionAmount>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
</cac:Price>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
<cbc:Amount currencyID="EUR">100.00</cbc:Amount>
</cac:AllowanceCharge>
</cac:InvoiceLine>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">171.00</cbc:TaxAmount>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">900.00</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">900.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">1071.00</cbc:TaxInclusiveAmount>
</cac:LegalMonetaryTotal>
</Invoice>`;
console.log('Testing complex calculation scenario');
try {
const { result: validation, metric } = await PerformanceTracker.track(
'complex-calculation-test',
async () => {
const einvoice = await EInvoice.fromXml(complexCalculationXml);
return await einvoice.validate();
}
);
console.log(`Complex calculation: ${validation.valid ? 'VALID' : 'INVALID'}`);
console.log(`Validation time: ${metric.duration.toFixed(2)}ms`);
console.log(`Calculation: 10×100 - 100 = 900, VAT: 171, Total: 1071`);
if (!validation.valid && validation.errors) {
const calcErrors = validation.errors.filter(e =>
e.message && e.message.toLowerCase().includes('calculation')
);
console.log(`Calculation issues found: ${calcErrors.length}`);
} else {
console.log(`Complex calculation validated successfully`);
}
// Should handle complex calculations efficiently
expect(metric.duration).toBeLessThan(100);
} catch (error) {
console.log(`Complex calculation test error: ${error.message}`);
}
});
tap.start();