539 lines
18 KiB
TypeScript
539 lines
18 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import * as plugins from '../../../ts/plugins.ts';
|
|
import { EInvoice } from '../../../ts/classes.xinvoice.ts';
|
|
import { CorpusLoader } from '../../helpers/corpus.loader.ts';
|
|
import { PerformanceTracker } from '../../helpers/performance.tracker.ts';
|
|
|
|
const testTimeout = 300000; // 5 minutes timeout for corpus processing
|
|
|
|
// VAL-11: Custom Validation Rules
|
|
// Tests custom validation rules that can be added beyond standard EN16931 rules
|
|
// Including organization-specific rules, industry-specific rules, and custom business logic
|
|
|
|
tap.test('VAL-11: Custom Validation Rules - Invoice Number Format Rules', async (tools) => {
|
|
const startTime = Date.now();
|
|
|
|
// Test custom invoice number format validation
|
|
const invoiceNumberRules = [
|
|
{
|
|
name: 'German Invoice Number Format (YYYY-NNNN)',
|
|
pattern: /^\d{4}-\d{4}$/,
|
|
testValues: [
|
|
{ value: '2024-0001', valid: true },
|
|
{ value: '2024-1234', valid: true },
|
|
{ value: '24-001', valid: false },
|
|
{ value: '2024-ABCD', valid: false },
|
|
{ value: 'INV-2024-001', valid: false },
|
|
{ value: '', valid: false }
|
|
]
|
|
},
|
|
{
|
|
name: 'Alphanumeric Invoice Format (INV-YYYY-NNNN)',
|
|
pattern: /^INV-\d{4}-\d{4}$/,
|
|
testValues: [
|
|
{ value: 'INV-2024-0001', valid: true },
|
|
{ value: 'INV-2024-1234', valid: true },
|
|
{ value: '2024-0001', valid: false },
|
|
{ value: 'inv-2024-0001', valid: false },
|
|
{ value: 'INV-24-001', valid: false }
|
|
]
|
|
}
|
|
];
|
|
|
|
for (const rule of invoiceNumberRules) {
|
|
tools.log(`Testing custom rule: ${rule.name}`);
|
|
|
|
for (const testValue of rule.testValues) {
|
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>${testValue.value}</ID>
|
|
<IssueDate>2024-01-01</IssueDate>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
|
</Invoice>`;
|
|
|
|
try {
|
|
const invoice = new EInvoice();
|
|
const parseResult = await invoice.fromXmlString(xml);
|
|
|
|
if (parseResult) {
|
|
// Apply custom validation rule
|
|
const isValid = rule.pattern.test(testValue.value);
|
|
|
|
if (testValue.valid) {
|
|
expect(isValid).toBeTrue();
|
|
tools.log(`✓ Valid format '${testValue.value}' accepted by ${rule.name}`);
|
|
} else {
|
|
expect(isValid).toBe(false);
|
|
tools.log(`✓ Invalid format '${testValue.value}' rejected by ${rule.name}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
tools.log(`Error testing '${testValue.value}': ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
PerformanceTracker.recordMetric('custom-validation-invoice-format', duration);
|
|
});
|
|
|
|
tap.test('VAL-11: Custom Validation Rules - Supplier Registration Validation', async (tools) => {
|
|
const startTime = Date.now();
|
|
|
|
// Test custom supplier registration number validation
|
|
const supplierValidationTests = [
|
|
{
|
|
name: 'German VAT Registration (DE + 9 digits)',
|
|
vatNumber: 'DE123456789',
|
|
country: 'DE',
|
|
valid: true
|
|
},
|
|
{
|
|
name: 'Austrian VAT Registration (ATU + 8 digits)',
|
|
vatNumber: 'ATU12345678',
|
|
country: 'AT',
|
|
valid: true
|
|
},
|
|
{
|
|
name: 'Invalid German VAT (wrong length)',
|
|
vatNumber: 'DE12345678',
|
|
country: 'DE',
|
|
valid: false
|
|
},
|
|
{
|
|
name: 'Invalid Country Code Format',
|
|
vatNumber: 'XX123456789',
|
|
country: 'XX',
|
|
valid: false
|
|
},
|
|
{
|
|
name: 'Missing VAT Number',
|
|
vatNumber: '',
|
|
country: 'DE',
|
|
valid: false
|
|
}
|
|
];
|
|
|
|
for (const test of supplierValidationTests) {
|
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>TEST-VAT-001</ID>
|
|
<IssueDate>2024-01-01</IssueDate>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
<AccountingSupplierParty>
|
|
<Party>
|
|
<PartyTaxScheme>
|
|
<CompanyID>${test.vatNumber}</CompanyID>
|
|
</PartyTaxScheme>
|
|
<PostalAddress>
|
|
<Country>
|
|
<IdentificationCode>${test.country}</IdentificationCode>
|
|
</Country>
|
|
</PostalAddress>
|
|
</Party>
|
|
</AccountingSupplierParty>
|
|
</Invoice>`;
|
|
|
|
try {
|
|
const invoice = new EInvoice();
|
|
const parseResult = await invoice.fromXmlString(xml);
|
|
|
|
if (parseResult) {
|
|
// Apply custom VAT validation rules
|
|
let isValidVAT = false;
|
|
|
|
if (test.country === 'DE' && test.vatNumber.length === 11 && test.vatNumber.startsWith('DE')) {
|
|
isValidVAT = /^DE\d{9}$/.test(test.vatNumber);
|
|
} else if (test.country === 'AT' && test.vatNumber.length === 11 && test.vatNumber.startsWith('ATU')) {
|
|
isValidVAT = /^ATU\d{8}$/.test(test.vatNumber);
|
|
}
|
|
|
|
if (test.valid) {
|
|
expect(isValidVAT).toBeTrue();
|
|
tools.log(`✓ ${test.name}: Valid VAT number accepted`);
|
|
} else {
|
|
expect(isValidVAT).toBe(false);
|
|
tools.log(`✓ ${test.name}: Invalid VAT number rejected`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (!test.valid) {
|
|
tools.log(`✓ ${test.name}: Invalid VAT properly rejected: ${error.message}`);
|
|
} else {
|
|
tools.log(`⚠ ${test.name}: Unexpected error: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
PerformanceTracker.recordMetric('custom-validation-vat', duration);
|
|
});
|
|
|
|
tap.test('VAL-11: Custom Validation Rules - Industry-Specific Rules', async (tools) => {
|
|
const startTime = Date.now();
|
|
|
|
// Test industry-specific validation rules (e.g., construction, healthcare)
|
|
const industryRules = [
|
|
{
|
|
name: 'Construction Industry - Project Reference Required',
|
|
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>CONSTRUCTION-001</ID>
|
|
<IssueDate>2024-01-01</IssueDate>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
<ProjectReference>
|
|
<ID>PROJ-2024-001</ID>
|
|
</ProjectReference>
|
|
<InvoiceLine>
|
|
<ID>1</ID>
|
|
<Item>
|
|
<Name>Construction Materials</Name>
|
|
<CommodityClassification>
|
|
<ItemClassificationCode listID="UNSPSC">30000000</ItemClassificationCode>
|
|
</CommodityClassification>
|
|
</Item>
|
|
</InvoiceLine>
|
|
</Invoice>`,
|
|
hasProjectReference: true,
|
|
isConstructionIndustry: true,
|
|
valid: true
|
|
},
|
|
{
|
|
name: 'Construction Industry - Missing Project Reference',
|
|
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>CONSTRUCTION-002</ID>
|
|
<IssueDate>2024-01-01</IssueDate>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
<InvoiceLine>
|
|
<ID>1</ID>
|
|
<Item>
|
|
<Name>Construction Materials</Name>
|
|
<CommodityClassification>
|
|
<ItemClassificationCode listID="UNSPSC">30000000</ItemClassificationCode>
|
|
</CommodityClassification>
|
|
</Item>
|
|
</InvoiceLine>
|
|
</Invoice>`,
|
|
hasProjectReference: false,
|
|
isConstructionIndustry: true,
|
|
valid: false
|
|
}
|
|
];
|
|
|
|
for (const test of industryRules) {
|
|
try {
|
|
const invoice = new EInvoice();
|
|
const parseResult = await invoice.fromXmlString(test.xml);
|
|
|
|
if (parseResult) {
|
|
// Apply custom industry-specific rules
|
|
let passesIndustryRules = true;
|
|
|
|
if (test.isConstructionIndustry) {
|
|
// Construction industry requires project reference
|
|
if (!test.hasProjectReference) {
|
|
passesIndustryRules = false;
|
|
}
|
|
}
|
|
|
|
if (test.valid) {
|
|
expect(passesIndustryRules).toBeTrue();
|
|
tools.log(`✓ ${test.name}: Industry rule compliance verified`);
|
|
} else {
|
|
expect(passesIndustryRules).toBe(false);
|
|
tools.log(`✓ ${test.name}: Industry rule violation detected`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (!test.valid) {
|
|
tools.log(`✓ ${test.name}: Industry rule violation properly caught: ${error.message}`);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
PerformanceTracker.recordMetric('custom-validation-industry', duration);
|
|
});
|
|
|
|
tap.test('VAL-11: Custom Validation Rules - Payment Terms Constraints', async (tools) => {
|
|
const startTime = Date.now();
|
|
|
|
// Test custom payment terms validation
|
|
const paymentConstraints = [
|
|
{
|
|
name: 'Maximum 60 days payment terms',
|
|
issueDate: '2024-01-01',
|
|
dueDate: '2024-02-29', // 59 days
|
|
maxDays: 60,
|
|
valid: true
|
|
},
|
|
{
|
|
name: 'Exceeds maximum payment terms',
|
|
issueDate: '2024-01-01',
|
|
dueDate: '2024-03-15', // 74 days
|
|
maxDays: 60,
|
|
valid: false
|
|
},
|
|
{
|
|
name: 'Weekend due date adjustment',
|
|
issueDate: '2024-01-01',
|
|
dueDate: '2024-01-06', // Saturday - should be adjusted to Monday
|
|
adjustWeekends: true,
|
|
valid: true
|
|
},
|
|
{
|
|
name: 'Early payment discount period',
|
|
issueDate: '2024-01-01',
|
|
dueDate: '2024-01-31',
|
|
earlyPaymentDate: '2024-01-10',
|
|
discountPercent: 2.0,
|
|
valid: true
|
|
}
|
|
];
|
|
|
|
for (const test of paymentConstraints) {
|
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>PAYMENT-TERMS-${Date.now()}</ID>
|
|
<IssueDate>${test.issueDate}</IssueDate>
|
|
<DueDate>${test.dueDate}</DueDate>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
<PaymentTerms>
|
|
<Note>Custom payment terms</Note>
|
|
${test.earlyPaymentDate ? `
|
|
<SettlementDiscountPercent>${test.discountPercent}</SettlementDiscountPercent>
|
|
<PenaltySurchargePercent>0</PenaltySurchargePercent>
|
|
` : ''}
|
|
</PaymentTerms>
|
|
</Invoice>`;
|
|
|
|
try {
|
|
const invoice = new EInvoice();
|
|
const parseResult = await invoice.fromXmlString(xml);
|
|
|
|
if (parseResult) {
|
|
// Apply custom payment terms validation
|
|
let passesPaymentRules = true;
|
|
|
|
if (test.maxDays) {
|
|
const issueDate = new Date(test.issueDate);
|
|
const dueDate = new Date(test.dueDate);
|
|
const daysDiff = Math.ceil((dueDate.getTime() - issueDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
|
if (daysDiff > test.maxDays) {
|
|
passesPaymentRules = false;
|
|
}
|
|
}
|
|
|
|
if (test.adjustWeekends) {
|
|
const dueDate = new Date(test.dueDate);
|
|
const dayOfWeek = dueDate.getDay();
|
|
// Weekend check (Saturday = 6, Sunday = 0)
|
|
if (dayOfWeek === 0 || dayOfWeek === 6) {
|
|
// This would normally trigger an adjustment rule
|
|
tools.log(`Due date falls on weekend: ${test.dueDate}`);
|
|
}
|
|
}
|
|
|
|
if (test.valid) {
|
|
expect(passesPaymentRules).toBeTrue();
|
|
tools.log(`✓ ${test.name}: Payment terms validation passed`);
|
|
} else {
|
|
expect(passesPaymentRules).toBe(false);
|
|
tools.log(`✓ ${test.name}: Payment terms validation failed as expected`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (!test.valid) {
|
|
tools.log(`✓ ${test.name}: Payment terms properly rejected: ${error.message}`);
|
|
} else {
|
|
tools.log(`⚠ ${test.name}: Unexpected error: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
PerformanceTracker.recordMetric('custom-validation-payment-terms', duration);
|
|
});
|
|
|
|
tap.test('VAL-11: Custom Validation Rules - Document Sequence Validation', async (tools) => {
|
|
const startTime = Date.now();
|
|
|
|
// Test custom document sequence validation
|
|
const sequenceTests = [
|
|
{
|
|
name: 'Valid Sequential Invoice Numbers',
|
|
invoices: [
|
|
{ id: 'INV-2024-0001', issueDate: '2024-01-01' },
|
|
{ id: 'INV-2024-0002', issueDate: '2024-01-02' },
|
|
{ id: 'INV-2024-0003', issueDate: '2024-01-03' }
|
|
],
|
|
valid: true
|
|
},
|
|
{
|
|
name: 'Gap in Invoice Sequence',
|
|
invoices: [
|
|
{ id: 'INV-2024-0001', issueDate: '2024-01-01' },
|
|
{ id: 'INV-2024-0003', issueDate: '2024-01-03' }, // Missing 0002
|
|
{ id: 'INV-2024-0004', issueDate: '2024-01-04' }
|
|
],
|
|
valid: false
|
|
},
|
|
{
|
|
name: 'Future-dated Invoice',
|
|
invoices: [
|
|
{ id: 'INV-2024-0001', issueDate: '2024-01-01' },
|
|
{ id: 'INV-2024-0002', issueDate: '2025-01-01' } // Future date
|
|
],
|
|
valid: false
|
|
}
|
|
];
|
|
|
|
for (const test of sequenceTests) {
|
|
try {
|
|
const invoiceNumbers = [];
|
|
const issueDates = [];
|
|
|
|
for (const invoiceData of test.invoices) {
|
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>${invoiceData.id}</ID>
|
|
<IssueDate>${invoiceData.issueDate}</IssueDate>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
</Invoice>`;
|
|
|
|
const invoice = new EInvoice();
|
|
const parseResult = await invoice.fromXmlString(xml);
|
|
|
|
if (parseResult) {
|
|
invoiceNumbers.push(invoiceData.id);
|
|
issueDates.push(new Date(invoiceData.issueDate));
|
|
}
|
|
}
|
|
|
|
// Apply custom sequence validation
|
|
let passesSequenceRules = true;
|
|
|
|
// Check for sequential numbering
|
|
for (let i = 1; i < invoiceNumbers.length; i++) {
|
|
const currentNumber = parseInt(invoiceNumbers[i].split('-').pop());
|
|
const previousNumber = parseInt(invoiceNumbers[i-1].split('-').pop());
|
|
|
|
if (currentNumber !== previousNumber + 1) {
|
|
passesSequenceRules = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check for future dates
|
|
const today = new Date();
|
|
for (const issueDate of issueDates) {
|
|
if (issueDate > today) {
|
|
passesSequenceRules = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (test.valid) {
|
|
expect(passesSequenceRules).toBeTrue();
|
|
tools.log(`✓ ${test.name}: Document sequence validation passed`);
|
|
} else {
|
|
expect(passesSequenceRules).toBe(false);
|
|
tools.log(`✓ ${test.name}: Document sequence validation failed as expected`);
|
|
}
|
|
|
|
} catch (error) {
|
|
if (!test.valid) {
|
|
tools.log(`✓ ${test.name}: Sequence validation properly rejected: ${error.message}`);
|
|
} else {
|
|
tools.log(`⚠ ${test.name}: Unexpected error: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
PerformanceTracker.recordMetric('custom-validation-sequence', duration);
|
|
});
|
|
|
|
tap.test('VAL-11: Custom Validation Rules - Corpus Custom Rules Application', { timeout: testTimeout }, async (tools) => {
|
|
const startTime = Date.now();
|
|
let processedFiles = 0;
|
|
let customRulesPassed = 0;
|
|
let customRulesViolations = 0;
|
|
|
|
try {
|
|
const ublFiles = await CorpusLoader.getFiles('UBL_XML_RECHNUNG');
|
|
|
|
for (const filePath of ublFiles.slice(0, 6)) { // Process first 6 files
|
|
try {
|
|
const invoice = new EInvoice();
|
|
const parseResult = await invoice.fromFile(filePath);
|
|
processedFiles++;
|
|
|
|
if (parseResult) {
|
|
// Apply a set of custom validation rules
|
|
let passesCustomRules = true;
|
|
|
|
// Custom Rule 1: Invoice ID must not be empty
|
|
// Custom Rule 2: Issue date must not be in the future
|
|
// Custom Rule 3: Currency code must be exactly 3 characters
|
|
|
|
const validationResult = await invoice.validate();
|
|
|
|
// For now, we'll consider the file passes custom rules if it passes standard validation
|
|
// In a real implementation, custom rules would be applied here
|
|
if (validationResult.valid) {
|
|
customRulesPassed++;
|
|
} else {
|
|
customRulesViolations++;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
tools.log(`Failed to process ${plugins.path.basename(filePath)}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
const customRulesSuccessRate = processedFiles > 0 ? (customRulesPassed / processedFiles) * 100 : 0;
|
|
const customRulesViolationRate = processedFiles > 0 ? (customRulesViolations / processedFiles) * 100 : 0;
|
|
|
|
tools.log(`Custom rules validation completed:`);
|
|
tools.log(`- Processed: ${processedFiles} files`);
|
|
tools.log(`- Passed custom rules: ${customRulesPassed} files (${customRulesSuccessRate.toFixed(1)}%)`);
|
|
tools.log(`- Custom rule violations: ${customRulesViolations} files (${customRulesViolationRate.toFixed(1)}%)`);
|
|
|
|
// Custom rules should have reasonable success rate
|
|
expect(customRulesSuccessRate).toBeGreaterThan(50);
|
|
|
|
} catch (error) {
|
|
tools.log(`Corpus custom validation failed: ${error.message}`);
|
|
throw error;
|
|
}
|
|
|
|
const totalDuration = Date.now() - startTime;
|
|
PerformanceTracker.recordMetric('custom-validation-corpus', totalDuration);
|
|
|
|
expect(totalDuration).toBeLessThan(90000); // 90 seconds max
|
|
tools.log(`Custom validation performance: ${totalDuration}ms total`);
|
|
});
|
|
|
|
tap.test('VAL-11: Performance Summary', async (tools) => {
|
|
const operations = [
|
|
'custom-validation-invoice-format',
|
|
'custom-validation-vat',
|
|
'custom-validation-industry',
|
|
'custom-validation-payment-terms',
|
|
'custom-validation-sequence',
|
|
'custom-validation-corpus'
|
|
];
|
|
|
|
for (const operation of operations) {
|
|
const summary = await PerformanceTracker.getSummary(operation);
|
|
if (summary) {
|
|
tools.log(`${operation}: avg=${summary.average}ms, min=${summary.min}ms, max=${summary.max}ms, p95=${summary.p95}ms`);
|
|
}
|
|
}
|
|
}); |