update
This commit is contained in:
440
test/suite/einvoice_validation/test.val-08.profile-validation.ts
Normal file
440
test/suite/einvoice_validation/test.val-08.profile-validation.ts
Normal file
@ -0,0 +1,440 @@
|
||||
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-08: Profile Validation - should validate format-specific profiles and customizations', async () => {
|
||||
// Test XRechnung profile validation
|
||||
const ublFiles = await CorpusLoader.getFiles('UBL_XMLRECHNUNG');
|
||||
const xrechnungFiles = ublFiles.filter(f =>
|
||||
path.basename(f).toLowerCase().includes('xrechnung')
|
||||
);
|
||||
|
||||
console.log(`Testing profile validation on ${xrechnungFiles.length} XRechnung files`);
|
||||
|
||||
const { EInvoice } = await import('../../../ts/index.js');
|
||||
|
||||
let validProfiles = 0;
|
||||
let invalidProfiles = 0;
|
||||
let errorCount = 0;
|
||||
const profileIssues: { file: string; profile?: string; issues: string[] }[] = [];
|
||||
|
||||
for (const filePath of xrechnungFiles.slice(0, 5)) { // Test first 5 files
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
try {
|
||||
const xmlContent = await fs.readFile(filePath, 'utf-8');
|
||||
|
||||
const { result: einvoice } = await PerformanceTracker.track(
|
||||
'profile-xml-loading',
|
||||
async () => await EInvoice.fromXml(xmlContent)
|
||||
);
|
||||
|
||||
// Extract profile information
|
||||
const profileInfo = extractProfileInfo(xmlContent);
|
||||
|
||||
const { result: validation } = await PerformanceTracker.track(
|
||||
'profile-validation',
|
||||
async () => {
|
||||
return await einvoice.validate(/* ValidationLevel.PROFILE */);
|
||||
},
|
||||
{
|
||||
file: fileName,
|
||||
profile: profileInfo.customizationId
|
||||
}
|
||||
);
|
||||
|
||||
if (validation.valid) {
|
||||
validProfiles++;
|
||||
console.log(`✓ ${fileName}: Profile valid (${profileInfo.customizationId || 'unknown'})`);
|
||||
} else {
|
||||
invalidProfiles++;
|
||||
|
||||
// Look for profile-specific errors
|
||||
const profErrors = validation.errors ? validation.errors.filter(e =>
|
||||
e.message && (
|
||||
e.message.toLowerCase().includes('profile') ||
|
||||
e.message.toLowerCase().includes('customization') ||
|
||||
e.message.toLowerCase().includes('xrechnung') ||
|
||||
e.code && e.code.includes('PROF')
|
||||
)
|
||||
) : [];
|
||||
|
||||
profileIssues.push({
|
||||
file: fileName,
|
||||
profile: profileInfo.customizationId,
|
||||
issues: profErrors.map(e => `${e.code}: ${e.message}`)
|
||||
});
|
||||
|
||||
console.log(`○ ${fileName}: Profile issues found (${profErrors.length})`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
console.log(`✗ ${fileName}: Error - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== PROFILE VALIDATION SUMMARY ===');
|
||||
console.log(`Valid profiles: ${validProfiles}`);
|
||||
console.log(`Invalid profiles: ${invalidProfiles}`);
|
||||
console.log(`Processing errors: ${errorCount}`);
|
||||
|
||||
// Show sample profile issues
|
||||
if (profileIssues.length > 0) {
|
||||
console.log('\nProfile issues detected:');
|
||||
profileIssues.slice(0, 3).forEach(item => {
|
||||
console.log(` ${item.file} (${item.profile || 'unknown'}):`);
|
||||
item.issues.slice(0, 2).forEach(issue => {
|
||||
console.log(` - ${issue}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Performance summary
|
||||
const perfSummary = await PerformanceTracker.getSummary('profile-validation');
|
||||
if (perfSummary) {
|
||||
console.log(`\nProfile Validation Performance:`);
|
||||
console.log(` Average: ${perfSummary.average.toFixed(2)}ms`);
|
||||
console.log(` P95: ${perfSummary.p95.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
expect(validProfiles + invalidProfiles).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('VAL-08: XRechnung Profile Validation - should validate XRechnung-specific requirements', async () => {
|
||||
const { EInvoice } = await import('../../../ts/index.js');
|
||||
|
||||
const xrechnungProfileTests = [
|
||||
{
|
||||
name: 'Valid XRechnung 3.0 profile',
|
||||
xml: `<?xml version="1.0"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
|
||||
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
|
||||
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_3.0</cbc:CustomizationID>
|
||||
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
|
||||
<cbc:ID>XR-2024-001</cbc:ID>
|
||||
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>German Supplier GmbH</cbc:Name>
|
||||
</cac:PartyName>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
</Invoice>`,
|
||||
shouldBeValid: true,
|
||||
profile: 'XRechnung 3.0'
|
||||
},
|
||||
{
|
||||
name: 'Missing CustomizationID',
|
||||
xml: `<?xml version="1.0"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
|
||||
<cbc:ID>XR-2024-002</cbc:ID>
|
||||
</Invoice>`,
|
||||
shouldBeValid: false,
|
||||
profile: 'Missing CustomizationID'
|
||||
},
|
||||
{
|
||||
name: 'Invalid XRechnung CustomizationID',
|
||||
xml: `<?xml version="1.0"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
<cbc:CustomizationID>urn:invalid:customization:id</cbc:CustomizationID>
|
||||
<cbc:ID>XR-2024-003</cbc:ID>
|
||||
</Invoice>`,
|
||||
shouldBeValid: false,
|
||||
profile: 'Invalid CustomizationID'
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of xrechnungProfileTests) {
|
||||
try {
|
||||
const { result: validation } = await PerformanceTracker.track(
|
||||
'xrechnung-profile-test',
|
||||
async () => {
|
||||
const einvoice = await EInvoice.fromXml(test.xml);
|
||||
return await einvoice.validate();
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`${test.name}: ${validation.valid ? 'VALID' : 'INVALID'}`);
|
||||
console.log(` Profile: ${test.profile}`);
|
||||
|
||||
if (!test.shouldBeValid && !validation.valid) {
|
||||
console.log(` ✓ Correctly detected profile violation`);
|
||||
if (validation.errors) {
|
||||
const profileErrors = validation.errors.filter(e =>
|
||||
e.message && (
|
||||
e.message.toLowerCase().includes('customization') ||
|
||||
e.message.toLowerCase().includes('profile') ||
|
||||
e.message.toLowerCase().includes('xrechnung')
|
||||
)
|
||||
);
|
||||
console.log(` Profile errors: ${profileErrors.length}`);
|
||||
}
|
||||
} else if (test.shouldBeValid && validation.valid) {
|
||||
console.log(` ✓ Correctly validated XRechnung profile`);
|
||||
} else {
|
||||
console.log(` ○ Unexpected result (XRechnung profile validation may need implementation)`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(`${test.name}: Error - ${error.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('VAL-08: Factur-X Profile Validation - should validate Factur-X profile requirements', async () => {
|
||||
const { EInvoice } = await import('../../../ts/index.js');
|
||||
|
||||
const facturxProfileTests = [
|
||||
{
|
||||
name: 'Valid Factur-X BASIC profile',
|
||||
xml: `<?xml version="1.0"?>
|
||||
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
|
||||
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
|
||||
<rsm:ExchangedDocumentContext>
|
||||
<ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
<ram:ID>urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic</ram:ID>
|
||||
</ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
</rsm:ExchangedDocumentContext>
|
||||
<rsm:ExchangedDocument>
|
||||
<ram:ID>FX-2024-001</ram:ID>
|
||||
<ram:TypeCode>380</ram:TypeCode>
|
||||
</rsm:ExchangedDocument>
|
||||
</rsm:CrossIndustryInvoice>`,
|
||||
shouldBeValid: true,
|
||||
profile: 'Factur-X BASIC'
|
||||
},
|
||||
{
|
||||
name: 'Valid Factur-X EN16931 profile',
|
||||
xml: `<?xml version="1.0"?>
|
||||
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
|
||||
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
|
||||
<rsm:ExchangedDocumentContext>
|
||||
<ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
<ram:ID>urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:en16931</ram:ID>
|
||||
</ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
</rsm:ExchangedDocumentContext>
|
||||
<rsm:ExchangedDocument>
|
||||
<ram:ID>FX-2024-002</ram:ID>
|
||||
<ram:TypeCode>380</ram:TypeCode>
|
||||
</rsm:ExchangedDocument>
|
||||
</rsm:CrossIndustryInvoice>`,
|
||||
shouldBeValid: true,
|
||||
profile: 'Factur-X EN16931'
|
||||
},
|
||||
{
|
||||
name: 'Missing guideline parameter',
|
||||
xml: `<?xml version="1.0"?>
|
||||
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
|
||||
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
|
||||
<rsm:ExchangedDocumentContext>
|
||||
<!-- Missing GuidelineSpecifiedDocumentContextParameter -->
|
||||
</rsm:ExchangedDocumentContext>
|
||||
<rsm:ExchangedDocument>
|
||||
<ram:ID>FX-2024-003</ram:ID>
|
||||
</rsm:ExchangedDocument>
|
||||
</rsm:CrossIndustryInvoice>`,
|
||||
shouldBeValid: false,
|
||||
profile: 'Missing guideline'
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of facturxProfileTests) {
|
||||
try {
|
||||
const { result: validation } = await PerformanceTracker.track(
|
||||
'facturx-profile-test',
|
||||
async () => {
|
||||
const einvoice = await EInvoice.fromXml(test.xml);
|
||||
return await einvoice.validate();
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`${test.name}: ${validation.valid ? 'VALID' : 'INVALID'}`);
|
||||
console.log(` Profile: ${test.profile}`);
|
||||
|
||||
if (!test.shouldBeValid && !validation.valid) {
|
||||
console.log(` ✓ Correctly detected Factur-X profile violation`);
|
||||
if (validation.errors) {
|
||||
const profileErrors = validation.errors.filter(e =>
|
||||
e.message && (
|
||||
e.message.toLowerCase().includes('guideline') ||
|
||||
e.message.toLowerCase().includes('profile') ||
|
||||
e.message.toLowerCase().includes('factur')
|
||||
)
|
||||
);
|
||||
console.log(` Factur-X profile errors: ${profileErrors.length}`);
|
||||
}
|
||||
} else if (test.shouldBeValid && validation.valid) {
|
||||
console.log(` ✓ Correctly validated Factur-X profile`);
|
||||
} else {
|
||||
console.log(` ○ Unexpected result (Factur-X profile validation may need implementation)`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(`${test.name}: Error - ${error.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('VAL-08: ZUGFeRD Profile Validation - should validate ZUGFeRD profile requirements', async () => {
|
||||
const { EInvoice } = await import('../../../ts/index.js');
|
||||
|
||||
const zugferdProfileTests = [
|
||||
{
|
||||
name: 'Valid ZUGFeRD BASIC profile',
|
||||
xml: `<?xml version="1.0"?>
|
||||
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
|
||||
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
|
||||
<rsm:ExchangedDocumentContext>
|
||||
<ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
<ram:ID>urn:zugferd:2p1:basic</ram:ID>
|
||||
</ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
</rsm:ExchangedDocumentContext>
|
||||
<rsm:ExchangedDocument>
|
||||
<ram:ID>ZF-2024-001</ram:ID>
|
||||
<ram:TypeCode>380</ram:TypeCode>
|
||||
</rsm:ExchangedDocument>
|
||||
</rsm:CrossIndustryInvoice>`,
|
||||
shouldBeValid: true,
|
||||
profile: 'ZUGFeRD BASIC'
|
||||
},
|
||||
{
|
||||
name: 'Valid ZUGFeRD COMFORT profile',
|
||||
xml: `<?xml version="1.0"?>
|
||||
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
|
||||
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
|
||||
<rsm:ExchangedDocumentContext>
|
||||
<ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
<ram:ID>urn:zugferd:2p1:comfort</ram:ID>
|
||||
</ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
</rsm:ExchangedDocumentContext>
|
||||
<rsm:ExchangedDocument>
|
||||
<ram:ID>ZF-2024-002</ram:ID>
|
||||
<ram:TypeCode>380</ram:TypeCode>
|
||||
</rsm:ExchangedDocument>
|
||||
</rsm:CrossIndustryInvoice>`,
|
||||
shouldBeValid: true,
|
||||
profile: 'ZUGFeRD COMFORT'
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of zugferdProfileTests) {
|
||||
try {
|
||||
const { result: validation } = await PerformanceTracker.track(
|
||||
'zugferd-profile-test',
|
||||
async () => {
|
||||
const einvoice = await EInvoice.fromXml(test.xml);
|
||||
return await einvoice.validate();
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`${test.name}: ${validation.valid ? 'VALID' : 'INVALID'}`);
|
||||
console.log(` Profile: ${test.profile}`);
|
||||
|
||||
// ZUGFeRD profile validation depends on implementation
|
||||
if (validation.valid) {
|
||||
console.log(` ✓ ZUGFeRD profile validation passed`);
|
||||
} else {
|
||||
console.log(` ○ ZUGFeRD profile validation (may need implementation)`);
|
||||
if (validation.errors) {
|
||||
const profileErrors = validation.errors.filter(e =>
|
||||
e.message && (
|
||||
e.message.toLowerCase().includes('zugferd') ||
|
||||
e.message.toLowerCase().includes('profile')
|
||||
)
|
||||
);
|
||||
console.log(` ZUGFeRD profile errors: ${profileErrors.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(`${test.name}: Error - ${error.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('VAL-08: Profile Compatibility Validation - should validate profile compatibility', async () => {
|
||||
const { EInvoice } = await import('../../../ts/index.js');
|
||||
|
||||
const compatibilityTests = [
|
||||
{
|
||||
name: 'Compatible profiles (EN16931 compliant)',
|
||||
xml: `<?xml version="1.0"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_3.0</cbc:CustomizationID>
|
||||
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
|
||||
<cbc:ID>COMPAT-001</cbc:ID>
|
||||
</Invoice>`,
|
||||
description: 'XRechnung with PEPPOL profile (compatible)'
|
||||
},
|
||||
{
|
||||
name: 'Mixed format indicators',
|
||||
xml: `<?xml version="1.0"?>
|
||||
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
|
||||
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
|
||||
<rsm:ExchangedDocumentContext>
|
||||
<ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
<ram:ID>urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic</ram:ID>
|
||||
</ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
<ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
<ram:ID>urn:zugferd:2p1:basic</ram:ID>
|
||||
</ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
</rsm:ExchangedDocumentContext>
|
||||
</rsm:CrossIndustryInvoice>`,
|
||||
description: 'Multiple conflicting profile indicators'
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of compatibilityTests) {
|
||||
try {
|
||||
const { result: validation } = await PerformanceTracker.track(
|
||||
'profile-compatibility-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.errors && validation.errors.length > 0) {
|
||||
const compatErrors = validation.errors.filter(e =>
|
||||
e.message && (
|
||||
e.message.toLowerCase().includes('compatible') ||
|
||||
e.message.toLowerCase().includes('conflict') ||
|
||||
e.message.toLowerCase().includes('profile')
|
||||
)
|
||||
);
|
||||
console.log(` Compatibility issues: ${compatErrors.length}`);
|
||||
} else {
|
||||
console.log(` No compatibility issues detected`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(`${test.name}: Error - ${error.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to extract profile information from XML
|
||||
function extractProfileInfo(xml: string): { customizationId?: string; profileId?: string } {
|
||||
const customizationMatch = xml.match(/<cbc:CustomizationID[^>]*>([^<]+)<\/cbc:CustomizationID>/);
|
||||
const profileMatch = xml.match(/<cbc:ProfileID[^>]*>([^<]+)<\/cbc:ProfileID>/);
|
||||
const guidelineMatch = xml.match(/<ram:ID[^>]*>([^<]+)<\/ram:ID>/);
|
||||
|
||||
return {
|
||||
customizationId: customizationMatch?.[1] || guidelineMatch?.[1],
|
||||
profileId: profileMatch?.[1]
|
||||
};
|
||||
}
|
||||
|
||||
tap.start();
|
Reference in New Issue
Block a user