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'; import { CorpusLoader } from '../corpus.loader.js'; const performanceTracker = new PerformanceTracker('STD-03: PEPPOL BIS 3.0 Compliance'); tap.test('STD-03: PEPPOL BIS 3.0 Compliance - should validate PEPPOL Business Interoperability Specifications', async (t) => { const einvoice = new EInvoice(); const corpusLoader = new CorpusLoader(); // Test 1: PEPPOL BIS 3.0 mandatory elements const peppolMandatoryElements = await performanceTracker.measureAsync( 'peppol-mandatory-elements', async () => { const peppolRequirements = [ 'CustomizationID', // Must be specific PEPPOL value 'ProfileID', // Must reference PEPPOL process 'EndpointID', // Both buyer and seller must have endpoints 'CompanyID', // VAT registration required 'SchemeID', // Proper scheme identifiers 'InvoicePeriod', // When applicable 'OrderReference', // Strongly recommended ]; const testCases = [ { name: 'complete-peppol-invoice', xml: createCompletePEPPOLInvoice() }, { name: 'missing-endpoint-ids', xml: createPEPPOLWithoutEndpoints() }, { name: 'invalid-customization-id', xml: createPEPPOLWithInvalidCustomization() }, { name: 'missing-scheme-ids', xml: createPEPPOLWithoutSchemeIds() } ]; const results = []; for (const test of testCases) { try { const parsed = await einvoice.parseDocument(test.xml); const validation = await einvoice.validatePEPPOLBIS(parsed); results.push({ testCase: test.name, valid: validation?.isValid || false, peppolCompliant: validation?.peppolCompliant || false, missingElements: validation?.missingElements || [], invalidElements: validation?.invalidElements || [], warnings: validation?.warnings || [] }); } catch (error) { results.push({ testCase: test.name, valid: false, error: error.message }); } } return results; } ); const completeTest = peppolMandatoryElements.find(r => r.testCase === 'complete-peppol-invoice'); t.ok(completeTest?.peppolCompliant, 'Complete PEPPOL invoice should be compliant'); peppolMandatoryElements.filter(r => r.testCase !== 'complete-peppol-invoice').forEach(result => { t.notOk(result.peppolCompliant, `${result.testCase} should not be PEPPOL compliant`); }); // Test 2: PEPPOL Participant Identifier validation const participantIdentifierValidation = await performanceTracker.measureAsync( 'participant-identifier-validation', async () => { const identifierTests = [ { name: 'valid-gln', scheme: '0088', identifier: '7300010000001', expected: { valid: true, type: 'GLN' } }, { name: 'valid-duns', scheme: '0060', identifier: '123456789', expected: { valid: true, type: 'DUNS' } }, { name: 'valid-orgnr', scheme: '0007', identifier: '123456789', expected: { valid: true, type: 'SE:ORGNR' } }, { name: 'invalid-scheme', scheme: '9999', identifier: '123456789', expected: { valid: false, error: 'Unknown scheme' } }, { name: 'invalid-checksum', scheme: '0088', identifier: '7300010000000', // Invalid GLN checksum expected: { valid: false, error: 'Invalid checksum' } } ]; const results = []; for (const test of identifierTests) { const invoice = createPEPPOLWithParticipant(test.scheme, test.identifier); const validation = await einvoice.validatePEPPOLParticipant(invoice); results.push({ test: test.name, scheme: test.scheme, identifier: test.identifier, valid: validation?.isValid || false, identifierType: validation?.identifierType, checksumValid: validation?.checksumValid, schemeRecognized: validation?.schemeRecognized }); } return results; } ); participantIdentifierValidation.forEach(result => { const expected = identifierTests.find(t => t.name === result.test)?.expected; t.equal(result.valid, expected?.valid, `Participant identifier ${result.test} validation should match expected`); }); // Test 3: PEPPOL Document Type validation const documentTypeValidation = await performanceTracker.measureAsync( 'peppol-document-type-validation', async () => { const documentTypes = [ { name: 'invoice', customizationId: 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0', profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0', valid: true }, { name: 'credit-note', customizationId: 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0', profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0', typeCode: '381', valid: true }, { name: 'old-bis2', customizationId: 'urn:www.cenbii.eu:transaction:biitrns010:ver2.0', profileId: 'urn:www.cenbii.eu:profile:bii05:ver2.0', valid: false // Old version } ]; const results = []; for (const docType of documentTypes) { const invoice = createPEPPOLWithDocumentType(docType); const validation = await einvoice.validatePEPPOLDocumentType(invoice); results.push({ documentType: docType.name, customizationId: docType.customizationId, profileId: docType.profileId, recognized: validation?.recognized || false, supported: validation?.supported || false, version: validation?.version, deprecated: validation?.deprecated || false }); } return results; } ); documentTypeValidation.forEach(result => { const expected = documentTypes.find(d => d.name === result.documentType); if (expected?.valid) { t.ok(result.supported, `Document type ${result.documentType} should be supported`); } else { t.notOk(result.supported || result.deprecated, `Document type ${result.documentType} should not be supported`); } }); // Test 4: PEPPOL Business Rules validation const businessRulesValidation = await performanceTracker.measureAsync( 'peppol-business-rules', async () => { const peppolRules = [ { rule: 'PEPPOL-EN16931-R001', description: 'Business process MUST be provided', violation: createInvoiceViolatingPEPPOLRule('R001') }, { rule: 'PEPPOL-EN16931-R002', description: 'Supplier electronic address MUST be provided', violation: createInvoiceViolatingPEPPOLRule('R002') }, { rule: 'PEPPOL-EN16931-R003', description: 'Customer electronic address MUST be provided', violation: createInvoiceViolatingPEPPOLRule('R003') }, { rule: 'PEPPOL-EN16931-R004', description: 'Specification identifier MUST have correct value', violation: createInvoiceViolatingPEPPOLRule('R004') }, { rule: 'PEPPOL-EN16931-R007', description: 'Payment means code must be valid', violation: createInvoiceViolatingPEPPOLRule('R007') } ]; const results = []; for (const ruleTest of peppolRules) { try { const parsed = await einvoice.parseDocument(ruleTest.violation); const validation = await einvoice.validatePEPPOLBusinessRules(parsed); const violation = validation?.violations?.find(v => v.rule === ruleTest.rule); results.push({ rule: ruleTest.rule, description: ruleTest.description, violated: !!violation, severity: violation?.severity || 'unknown', flag: violation?.flag || 'unknown' // fatal/warning }); } catch (error) { results.push({ rule: ruleTest.rule, error: error.message }); } } return results; } ); businessRulesValidation.forEach(result => { t.ok(result.violated, `PEPPOL rule ${result.rule} violation should be detected`); }); // Test 5: PEPPOL Code List validation const codeListValidation = await performanceTracker.measureAsync( 'peppol-code-list-validation', async () => { const codeTests = [ { list: 'ICD', code: '0088', description: 'GLN', valid: true }, { list: 'EAS', code: '9906', description: 'IT:VAT', valid: true }, { list: 'UNCL1001', code: '380', description: 'Commercial invoice', valid: true }, { list: 'ISO3166', code: 'NO', description: 'Norway', valid: true }, { list: 'UNCL4461', code: '42', description: 'Payment to bank account', valid: true } ]; const results = []; for (const test of codeTests) { const validation = await einvoice.validatePEPPOLCode(test.list, test.code); results.push({ list: test.list, code: test.code, description: test.description, valid: validation?.isValid || false, recognized: validation?.recognized || false, deprecated: validation?.deprecated || false }); } return results; } ); codeListValidation.forEach(result => { t.ok(result.valid && result.recognized, `PEPPOL code ${result.code} in list ${result.list} should be valid`); }); // Test 6: PEPPOL Transport validation const transportValidation = await performanceTracker.measureAsync( 'peppol-transport-validation', async () => { const transportTests = [ { name: 'as4-compliant', endpoint: 'https://ap.example.com/as4', certificate: 'valid-peppol-cert', encryption: 'required' }, { name: 'smp-lookup', participantId: '0007:123456789', documentType: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1' }, { name: 'certificate-validation', cert: 'PEPPOL-SMP-cert', ca: 'PEPPOL-Root-CA' } ]; const results = []; for (const test of transportTests) { const validation = await einvoice.validatePEPPOLTransport(test); results.push({ test: test.name, transportReady: validation?.transportReady || false, endpointValid: validation?.endpointValid || false, certificateValid: validation?.certificateValid || false, smpResolvable: validation?.smpResolvable || false }); } return results; } ); transportValidation.forEach(result => { t.ok(result.transportReady, `PEPPOL transport ${result.test} should be ready`); }); // Test 7: PEPPOL MLR (Message Level Response) handling const mlrHandling = await performanceTracker.measureAsync( 'peppol-mlr-handling', async () => { const mlrScenarios = [ { name: 'invoice-response-accept', responseCode: 'AP', status: 'Accepted' }, { name: 'invoice-response-reject', responseCode: 'RE', status: 'Rejected', reasons: ['Missing mandatory field', 'Invalid VAT calculation'] }, { name: 'invoice-response-conditional', responseCode: 'CA', status: 'Conditionally Accepted', conditions: ['Payment terms clarification needed'] } ]; const results = []; for (const scenario of mlrScenarios) { const mlr = createPEPPOLMLR(scenario); const validation = await einvoice.validatePEPPOLMLR(mlr); results.push({ scenario: scenario.name, responseCode: scenario.responseCode, valid: validation?.isValid || false, structureValid: validation?.structureValid || false, semanticsValid: validation?.semanticsValid || false }); } return results; } ); mlrHandling.forEach(result => { t.ok(result.valid, `PEPPOL MLR ${result.scenario} should be valid`); }); // Test 8: PEPPOL Directory integration const directoryIntegration = await performanceTracker.measureAsync( 'peppol-directory-integration', async () => { const directoryTests = [ { name: 'participant-lookup', identifier: '0007:987654321', country: 'NO' }, { name: 'capability-lookup', participant: '0088:7300010000001', documentTypes: ['Invoice', 'CreditNote', 'OrderResponse'] }, { name: 'smp-metadata', endpoint: 'https://smp.example.com', participant: '0184:IT01234567890' } ]; const results = []; for (const test of directoryTests) { const lookup = await einvoice.lookupPEPPOLParticipant(test); results.push({ test: test.name, found: lookup?.found || false, active: lookup?.active || false, capabilities: lookup?.capabilities || [], metadata: lookup?.metadata || {} }); } return results; } ); directoryIntegration.forEach(result => { t.ok(result.found !== undefined, `PEPPOL directory lookup ${result.test} should return result`); }); // Test 9: Corpus PEPPOL validation const corpusPEPPOLValidation = await performanceTracker.measureAsync( 'corpus-peppol-validation', async () => { const peppolFiles = await corpusLoader.getFilesByPattern('**/PEPPOL/**/*.xml'); const results = { total: peppolFiles.length, valid: 0, invalid: 0, errors: [], profiles: {} }; for (const file of peppolFiles.slice(0, 10)) { // Test first 10 try { const content = await corpusLoader.readFile(file); const parsed = await einvoice.parseDocument(content); const validation = await einvoice.validatePEPPOLBIS(parsed); if (validation?.isValid) { results.valid++; const profile = validation.profileId || 'unknown'; results.profiles[profile] = (results.profiles[profile] || 0) + 1; } else { results.invalid++; results.errors.push({ file: file.name, errors: validation?.errors?.slice(0, 3) }); } } catch (error) { results.invalid++; results.errors.push({ file: file.name, error: error.message }); } } return results; } ); t.ok(corpusPEPPOLValidation.valid > 0, 'Some corpus files should be valid PEPPOL'); // Test 10: PEPPOL Country Specific Rules const countrySpecificRules = await performanceTracker.measureAsync( 'peppol-country-specific-rules', async () => { const countryTests = [ { country: 'IT', name: 'Italy', specificRules: ['Codice Fiscale required', 'SDI code mandatory'], invoice: createPEPPOLItalianInvoice() }, { country: 'NO', name: 'Norway', specificRules: ['Organization number format', 'Foretaksregisteret validation'], invoice: createPEPPOLNorwegianInvoice() }, { country: 'NL', name: 'Netherlands', specificRules: ['KvK number validation', 'OB number format'], invoice: createPEPPOLDutchInvoice() } ]; const results = []; for (const test of countryTests) { try { const parsed = await einvoice.parseDocument(test.invoice); const validation = await einvoice.validatePEPPOLCountryRules(parsed, test.country); results.push({ country: test.country, name: test.name, valid: validation?.isValid || false, countryRulesApplied: validation?.countryRulesApplied || false, specificValidations: validation?.specificValidations || [], violations: validation?.violations || [] }); } catch (error) { results.push({ country: test.country, name: test.name, error: error.message }); } } return results; } ); countrySpecificRules.forEach(result => { t.ok(result.countryRulesApplied, `Country specific rules for ${result.name} should be applied`); }); // Print performance summary performanceTracker.printSummary(); }); // Helper functions function createCompletePEPPOLInvoice(): string { return ` urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 PEPPOL-INV-001 2024-01-15 2024-02-15 380 EUR PO-12345 7300010000001 7300010000001 Supplier Company AS Main Street 1 Oslo 0001 NO NO999888777 VAT Supplier Company AS 999888777 123456789 123456789 Customer Company AB Storgatan 1 Stockholm 10001 SE 42 NO9386011117947 1000.00 1000.00 1250.00 1250.00 1 10 1000.00 Product A S 25 VAT 100.00 `; } function createPEPPOLWithoutEndpoints(): string { let invoice = createCompletePEPPOLInvoice(); // Remove endpoint IDs invoice = invoice.replace(/]*>.*?<\/cbc:EndpointID>/g, ''); return invoice; } function createPEPPOLWithInvalidCustomization(): string { let invoice = createCompletePEPPOLInvoice(); return invoice.replace( 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0', 'urn:cen.eu:en16931:2017' ); } function createPEPPOLWithoutSchemeIds(): string { let invoice = createCompletePEPPOLInvoice(); // Remove schemeID attributes invoice = invoice.replace(/ schemeID="[^"]*"/g, ''); return invoice; } function createPEPPOLWithParticipant(scheme: string, identifier: string): any { return { supplierEndpointID: { schemeID: scheme, value: identifier }, supplierPartyIdentification: { schemeID: scheme, value: identifier } }; } function createPEPPOLWithDocumentType(docType: any): string { let invoice = createCompletePEPPOLInvoice(); invoice = invoice.replace( /.*?<\/cbc:CustomizationID>/, `${docType.customizationId}` ); invoice = invoice.replace( /.*?<\/cbc:ProfileID>/, `${docType.profileId}` ); if (docType.typeCode) { invoice = invoice.replace( '380', `${docType.typeCode}` ); } return invoice; } function createInvoiceViolatingPEPPOLRule(rule: string): string { let invoice = createCompletePEPPOLInvoice(); switch (rule) { case 'R001': // Remove ProfileID return invoice.replace(/.*?<\/cbc:ProfileID>/, ''); case 'R002': // Remove supplier endpoint return invoice.replace(/7300010000001<\/cbc:EndpointID>/, ''); case 'R003': // Remove customer endpoint return invoice.replace(/123456789<\/cbc:EndpointID>/, ''); case 'R004': // Invalid CustomizationID return invoice.replace( 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0', 'invalid-customization-id' ); case 'R007': // Invalid payment means code return invoice.replace( '42', '99' ); default: return invoice; } } function createPEPPOLMLR(scenario: any): string { return ` urn:fdc:peppol.eu:poacc:trns:invoice_response:3 urn:fdc:peppol.eu:poacc:bis:invoice_response:3 MLR-${scenario.name} 2024-01-16 ${scenario.responseCode} ${scenario.responseCode} ${scenario.status} `; } function createPEPPOLItalianInvoice(): string { let invoice = createCompletePEPPOLInvoice(); // Add Italian specific fields const italianFields = ` RSSMRA85M01H501Z UFY9MH `; return invoice.replace('', italianFields + '\n '); } function createPEPPOLNorwegianInvoice(): string { // Already uses Norwegian example return createCompletePEPPOLInvoice(); } function createPEPPOLDutchInvoice(): string { let invoice = createCompletePEPPOLInvoice(); // Change to Dutch context invoice = invoice.replace('NO999888777', 'NL123456789B01'); invoice = invoice.replace('NO', 'NL'); invoice = invoice.replace('Oslo', 'Amsterdam'); invoice = invoice.replace('0001', '1011AB'); // Add KvK number const kvkNumber = '12345678'; invoice = invoice.replace('', kvkNumber + '\n '); return invoice; } const identifierTests = [ { name: 'valid-gln', scheme: '0088', identifier: '7300010000001', expected: { valid: true } }, { name: 'valid-duns', scheme: '0060', identifier: '123456789', expected: { valid: true } }, { name: 'valid-orgnr', scheme: '0007', identifier: '123456789', expected: { valid: true } }, { name: 'invalid-scheme', scheme: '9999', identifier: '123456789', expected: { valid: false } }, { name: 'invalid-checksum', scheme: '0088', identifier: '7300010000000', expected: { valid: false } } ]; const documentTypes = [ { name: 'invoice', customizationId: 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0', profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0', valid: true }, { name: 'credit-note', customizationId: 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0', profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0', typeCode: '381', valid: true }, { name: 'old-bis2', customizationId: 'urn:www.cenbii.eu:transaction:biitrns010:ver2.0', profileId: 'urn:www.cenbii.eu:profile:bii05:ver2.0', valid: false } ]; // Run the test tap.start();