440 lines
16 KiB
TypeScript
440 lines
16 KiB
TypeScript
|
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();
|