838 lines
27 KiB
TypeScript
838 lines
27 KiB
TypeScript
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 `<?xml version="1.0" encoding="UTF-8"?>
|
|
<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:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>
|
|
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
|
|
|
|
<cbc:ID>PEPPOL-INV-001</cbc:ID>
|
|
<cbc:IssueDate>2024-01-15</cbc:IssueDate>
|
|
<cbc:DueDate>2024-02-15</cbc:DueDate>
|
|
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
|
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
|
|
|
<cac:OrderReference>
|
|
<cbc:ID>PO-12345</cbc:ID>
|
|
</cac:OrderReference>
|
|
|
|
<cac:AccountingSupplierParty>
|
|
<cac:Party>
|
|
<cbc:EndpointID schemeID="0088">7300010000001</cbc:EndpointID>
|
|
<cac:PartyIdentification>
|
|
<cbc:ID schemeID="0088">7300010000001</cbc:ID>
|
|
</cac:PartyIdentification>
|
|
<cac:PartyName>
|
|
<cbc:Name>Supplier Company AS</cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PostalAddress>
|
|
<cbc:StreetName>Main Street 1</cbc:StreetName>
|
|
<cbc:CityName>Oslo</cbc:CityName>
|
|
<cbc:PostalZone>0001</cbc:PostalZone>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>NO</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
<cac:PartyTaxScheme>
|
|
<cbc:CompanyID>NO999888777</cbc:CompanyID>
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:PartyTaxScheme>
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName>Supplier Company AS</cbc:RegistrationName>
|
|
<cbc:CompanyID schemeID="0007">999888777</cbc:CompanyID>
|
|
</cac:PartyLegalEntity>
|
|
</cac:Party>
|
|
</cac:AccountingSupplierParty>
|
|
|
|
<cac:AccountingCustomerParty>
|
|
<cac:Party>
|
|
<cbc:EndpointID schemeID="0007">123456789</cbc:EndpointID>
|
|
<cac:PartyIdentification>
|
|
<cbc:ID schemeID="0007">123456789</cbc:ID>
|
|
</cac:PartyIdentification>
|
|
<cac:PartyName>
|
|
<cbc:Name>Customer Company AB</cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PostalAddress>
|
|
<cbc:StreetName>Storgatan 1</cbc:StreetName>
|
|
<cbc:CityName>Stockholm</cbc:CityName>
|
|
<cbc:PostalZone>10001</cbc:PostalZone>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>SE</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
</cac:Party>
|
|
</cac:AccountingCustomerParty>
|
|
|
|
<cac:PaymentMeans>
|
|
<cbc:PaymentMeansCode>42</cbc:PaymentMeansCode>
|
|
<cac:PayeeFinancialAccount>
|
|
<cbc:ID>NO9386011117947</cbc:ID>
|
|
</cac:PayeeFinancialAccount>
|
|
</cac:PaymentMeans>
|
|
|
|
<cac:LegalMonetaryTotal>
|
|
<cbc:LineExtensionAmount currencyID="EUR">1000.00</cbc:LineExtensionAmount>
|
|
<cbc:TaxExclusiveAmount currencyID="EUR">1000.00</cbc:TaxExclusiveAmount>
|
|
<cbc:TaxInclusiveAmount currencyID="EUR">1250.00</cbc:TaxInclusiveAmount>
|
|
<cbc:PayableAmount currencyID="EUR">1250.00</cbc:PayableAmount>
|
|
</cac:LegalMonetaryTotal>
|
|
|
|
<cac:InvoiceLine>
|
|
<cbc:ID>1</cbc:ID>
|
|
<cbc:InvoicedQuantity unitCode="C62">10</cbc:InvoicedQuantity>
|
|
<cbc:LineExtensionAmount currencyID="EUR">1000.00</cbc:LineExtensionAmount>
|
|
<cac:Item>
|
|
<cbc:Name>Product A</cbc:Name>
|
|
<cac:ClassifiedTaxCategory>
|
|
<cbc:ID>S</cbc:ID>
|
|
<cbc:Percent>25</cbc:Percent>
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:ClassifiedTaxCategory>
|
|
</cac:Item>
|
|
<cac:Price>
|
|
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
|
|
</cac:Price>
|
|
</cac:InvoiceLine>
|
|
</Invoice>`;
|
|
}
|
|
|
|
function createPEPPOLWithoutEndpoints(): string {
|
|
let invoice = createCompletePEPPOLInvoice();
|
|
// Remove endpoint IDs
|
|
invoice = invoice.replace(/<cbc:EndpointID[^>]*>.*?<\/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>.*?<\/cbc:CustomizationID>/,
|
|
`<cbc:CustomizationID>${docType.customizationId}</cbc:CustomizationID>`
|
|
);
|
|
invoice = invoice.replace(
|
|
/<cbc:ProfileID>.*?<\/cbc:ProfileID>/,
|
|
`<cbc:ProfileID>${docType.profileId}</cbc:ProfileID>`
|
|
);
|
|
if (docType.typeCode) {
|
|
invoice = invoice.replace(
|
|
'<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>',
|
|
`<cbc:InvoiceTypeCode>${docType.typeCode}</cbc:InvoiceTypeCode>`
|
|
);
|
|
}
|
|
return invoice;
|
|
}
|
|
|
|
function createInvoiceViolatingPEPPOLRule(rule: string): string {
|
|
let invoice = createCompletePEPPOLInvoice();
|
|
|
|
switch (rule) {
|
|
case 'R001':
|
|
// Remove ProfileID
|
|
return invoice.replace(/<cbc:ProfileID>.*?<\/cbc:ProfileID>/, '');
|
|
case 'R002':
|
|
// Remove supplier endpoint
|
|
return invoice.replace(/<cbc:EndpointID schemeID="0088">7300010000001<\/cbc:EndpointID>/, '');
|
|
case 'R003':
|
|
// Remove customer endpoint
|
|
return invoice.replace(/<cbc:EndpointID schemeID="0007">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(
|
|
'<cbc:PaymentMeansCode>42</cbc:PaymentMeansCode>',
|
|
'<cbc:PaymentMeansCode>99</cbc:PaymentMeansCode>'
|
|
);
|
|
default:
|
|
return invoice;
|
|
}
|
|
}
|
|
|
|
function createPEPPOLMLR(scenario: any): string {
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<ApplicationResponse xmlns="urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2">
|
|
<cbc:CustomizationID>urn:fdc:peppol.eu:poacc:trns:invoice_response:3</cbc:CustomizationID>
|
|
<cbc:ProfileID>urn:fdc:peppol.eu:poacc:bis:invoice_response:3</cbc:ProfileID>
|
|
<cbc:ID>MLR-${scenario.name}</cbc:ID>
|
|
<cbc:IssueDate>2024-01-16</cbc:IssueDate>
|
|
<cbc:ResponseCode>${scenario.responseCode}</cbc:ResponseCode>
|
|
<cac:DocumentResponse>
|
|
<cac:Response>
|
|
<cbc:ResponseCode>${scenario.responseCode}</cbc:ResponseCode>
|
|
<cbc:Description>${scenario.status}</cbc:Description>
|
|
</cac:Response>
|
|
</cac:DocumentResponse>
|
|
</ApplicationResponse>`;
|
|
}
|
|
|
|
function createPEPPOLItalianInvoice(): string {
|
|
let invoice = createCompletePEPPOLInvoice();
|
|
// Add Italian specific fields
|
|
const italianFields = `
|
|
<cac:PartyIdentification>
|
|
<cbc:ID schemeID="IT:CF">RSSMRA85M01H501Z</cbc:ID>
|
|
</cac:PartyIdentification>
|
|
<cac:PartyIdentification>
|
|
<cbc:ID schemeID="IT:IPA">UFY9MH</cbc:ID>
|
|
</cac:PartyIdentification>`;
|
|
|
|
return invoice.replace('</cac:Party>', italianFields + '\n </cac:Party>');
|
|
}
|
|
|
|
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('<cbc:IdentificationCode>NO</cbc:IdentificationCode>',
|
|
'<cbc:IdentificationCode>NL</cbc:IdentificationCode>');
|
|
invoice = invoice.replace('Oslo', 'Amsterdam');
|
|
invoice = invoice.replace('0001', '1011AB');
|
|
|
|
// Add KvK number
|
|
const kvkNumber = '<cbc:CompanyID schemeID="NL:KVK">12345678</cbc:CompanyID>';
|
|
invoice = invoice.replace('</cac:PartyLegalEntity>',
|
|
kvkNumber + '\n </cac:PartyLegalEntity>');
|
|
|
|
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(); |