einvoice/test/suite/einvoice_standards-compliance/test.std-02.xrechnung-cius.ts

792 lines
26 KiB
TypeScript
Raw Normal View History

2025-05-26 04:04:51 +00:00
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-02: XRechnung CIUS Compliance');
tap.test('STD-02: XRechnung CIUS Compliance - should validate XRechnung Core Invoice Usage Specification', async (t) => {
const einvoice = new EInvoice();
const corpusLoader = new CorpusLoader();
// Test 1: XRechnung specific mandatory fields
const xrechnungMandatoryFields = await performanceTracker.measureAsync(
'xrechnung-mandatory-fields',
async () => {
const xrechnungSpecificFields = [
'BT-10', // Buyer reference (mandatory in XRechnung)
'BT-23', // Business process
'BT-24', // Specification identifier (must be specific value)
'BT-49', // Buyer electronic address
'BT-34', // Seller electronic address
'BG-4', // Seller postal address (all sub-elements mandatory)
'BG-8', // Buyer postal address (all sub-elements mandatory)
];
const testCases = [
{
name: 'complete-xrechnung',
xml: createCompleteXRechnungInvoice()
},
{
name: 'missing-buyer-reference',
xml: createXRechnungWithoutField('BT-10')
},
{
name: 'missing-electronic-addresses',
xml: createXRechnungWithoutField(['BT-49', 'BT-34'])
},
{
name: 'incomplete-postal-address',
xml: createXRechnungWithIncompleteAddress()
}
];
const results = [];
for (const test of testCases) {
try {
const parsed = await einvoice.parseDocument(test.xml);
const validation = await einvoice.validateXRechnung(parsed);
results.push({
testCase: test.name,
valid: validation?.isValid || false,
xrechnungCompliant: validation?.xrechnungCompliant || false,
missingFields: validation?.missingXRechnungFields || [],
errors: validation?.errors || []
});
} catch (error) {
results.push({
testCase: test.name,
valid: false,
error: error.message
});
}
}
return results;
}
);
const completeTest = xrechnungMandatoryFields.find(r => r.testCase === 'complete-xrechnung');
t.ok(completeTest?.xrechnungCompliant, 'Complete XRechnung invoice should be compliant');
xrechnungMandatoryFields.filter(r => r.testCase !== 'complete-xrechnung').forEach(result => {
t.notOk(result.xrechnungCompliant, `${result.testCase} should not be XRechnung compliant`);
t.ok(result.missingFields?.length > 0, 'Missing XRechnung fields should be detected');
});
// Test 2: XRechnung specific business rules
const xrechnungBusinessRules = await performanceTracker.measureAsync(
'xrechnung-business-rules',
async () => {
const xrechnungRules = [
{
rule: 'BR-DE-1',
description: 'Payment account must be provided for credit transfer',
test: createInvoiceViolatingXRechnungRule('BR-DE-1')
},
{
rule: 'BR-DE-2',
description: 'Buyer reference is mandatory',
test: createInvoiceViolatingXRechnungRule('BR-DE-2')
},
{
rule: 'BR-DE-3',
description: 'Specification identifier must be correct',
test: createInvoiceViolatingXRechnungRule('BR-DE-3')
},
{
rule: 'BR-DE-15',
description: 'Buyer electronic address must be provided',
test: createInvoiceViolatingXRechnungRule('BR-DE-15')
},
{
rule: 'BR-DE-21',
description: 'VAT identifier format must be correct',
test: createInvoiceViolatingXRechnungRule('BR-DE-21')
}
];
const results = [];
for (const ruleTest of xrechnungRules) {
try {
const parsed = await einvoice.parseDocument(ruleTest.test);
const validation = await einvoice.validateXRechnungBusinessRules(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',
message: violation?.message
});
} catch (error) {
results.push({
rule: ruleTest.rule,
error: error.message
});
}
}
return results;
}
);
xrechnungBusinessRules.forEach(result => {
t.ok(result.violated, `XRechnung rule ${result.rule} violation should be detected`);
});
// Test 3: Leitweg-ID validation
const leitwegIdValidation = await performanceTracker.measureAsync(
'leitweg-id-validation',
async () => {
const leitwegTests = [
{
name: 'valid-format',
leitwegId: '04011000-12345-67',
expected: { valid: true }
},
{
name: 'valid-with-extension',
leitwegId: '04011000-12345-67-001',
expected: { valid: true }
},
{
name: 'invalid-checksum',
leitwegId: '04011000-12345-99',
expected: { valid: false, error: 'checksum' }
},
{
name: 'invalid-format',
leitwegId: '12345',
expected: { valid: false, error: 'format' }
},
{
name: 'missing-leitweg',
leitwegId: null,
expected: { valid: false, error: 'missing' }
}
];
const results = [];
for (const test of leitwegTests) {
const invoice = createXRechnungWithLeitwegId(test.leitwegId);
const validation = await einvoice.validateLeitwegId(invoice);
results.push({
test: test.name,
leitwegId: test.leitwegId,
valid: validation?.isValid || false,
checksumValid: validation?.checksumValid,
formatValid: validation?.formatValid,
error: validation?.error
});
}
return results;
}
);
leitwegIdValidation.forEach(result => {
const expected = leitwegTests.find(t => t.name === result.test)?.expected;
t.equal(result.valid, expected?.valid, `Leitweg-ID ${result.test} validation should match expected`);
});
// Test 4: XRechnung version compliance
const versionCompliance = await performanceTracker.measureAsync(
'xrechnung-version-compliance',
async () => {
const versions = [
{
version: '1.2',
specId: 'urn:cen.eu:en16931:2017:compliant:xoev-de:kosit:standard:xrechnung_1.2',
supported: false
},
{
version: '2.0',
specId: 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0',
supported: true
},
{
version: '2.3',
specId: 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3',
supported: true
},
{
version: '3.0',
specId: 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_3.0',
supported: true
}
];
const results = [];
for (const ver of versions) {
const invoice = createXRechnungWithVersion(ver.specId);
const validation = await einvoice.validateXRechnungVersion(invoice);
results.push({
version: ver.version,
specId: ver.specId,
recognized: validation?.versionRecognized || false,
supported: validation?.versionSupported || false,
deprecated: validation?.deprecated || false,
migrationPath: validation?.migrationPath
});
}
return results;
}
);
versionCompliance.forEach(result => {
const expected = versions.find(v => v.version === result.version);
if (expected?.supported) {
t.ok(result.supported, `XRechnung version ${result.version} should be supported`);
}
});
// Test 5: Code list restrictions
const codeListRestrictions = await performanceTracker.measureAsync(
'xrechnung-code-list-restrictions',
async () => {
const codeTests = [
{
field: 'Payment means',
code: '1', // Instrument not defined
allowed: false,
alternative: '58' // SEPA credit transfer
},
{
field: 'Tax category',
code: 'B', // Split payment
allowed: false,
alternative: 'S' // Standard rate
},
{
field: 'Invoice type',
code: '384', // Corrected invoice
allowed: false,
alternative: '380' // Commercial invoice
}
];
const results = [];
for (const test of codeTests) {
const invoice = createXRechnungWithCode(test.field, test.code);
const validation = await einvoice.validateXRechnungCodeLists(invoice);
const alternative = createXRechnungWithCode(test.field, test.alternative);
const altValidation = await einvoice.validateXRechnungCodeLists(alternative);
results.push({
field: test.field,
code: test.code,
rejected: !validation?.isValid,
alternativeCode: test.alternative,
alternativeAccepted: altValidation?.isValid || false,
reason: validation?.codeListErrors?.[0]
});
}
return results;
}
);
codeListRestrictions.forEach(result => {
t.ok(result.rejected, `Restricted code ${result.code} for ${result.field} should be rejected`);
t.ok(result.alternativeAccepted, `Alternative code ${result.alternativeCode} should be accepted`);
});
// Test 6: XRechnung extension handling
const extensionHandling = await performanceTracker.measureAsync(
'xrechnung-extension-handling',
async () => {
const extensionTests = [
{
name: 'ublex-extension',
xml: createXRechnungWithUBLExtension()
},
{
name: 'additional-doc-ref',
xml: createXRechnungWithAdditionalDocRef()
},
{
name: 'custom-fields',
xml: createXRechnungWithCustomFields()
}
];
const results = [];
for (const test of extensionTests) {
try {
const parsed = await einvoice.parseDocument(test.xml);
const validation = await einvoice.validateXRechnungWithExtensions(parsed);
results.push({
extension: test.name,
valid: validation?.isValid || false,
coreCompliant: validation?.coreXRechnungValid || false,
extensionAllowed: validation?.extensionAllowed || false,
extensionPreserved: validation?.extensionDataIntact || false
});
} catch (error) {
results.push({
extension: test.name,
error: error.message
});
}
}
return results;
}
);
extensionHandling.forEach(result => {
t.ok(result.coreCompliant, `Core XRechnung should remain valid with ${result.extension}`);
});
// Test 7: KOSIT validator compatibility
const kositValidatorCompatibility = await performanceTracker.measureAsync(
'kosit-validator-compatibility',
async () => {
const kositScenarios = [
{
name: 'standard-invoice',
scenario: 'EN16931 CIUS XRechnung (UBL Invoice)'
},
{
name: 'credit-note',
scenario: 'EN16931 CIUS XRechnung (UBL CreditNote)'
},
{
name: 'cii-invoice',
scenario: 'EN16931 CIUS XRechnung (CII)'
}
];
const results = [];
for (const scenario of kositScenarios) {
const invoice = createInvoiceForKOSITScenario(scenario.name);
const validation = await einvoice.validateWithKOSITRules(invoice);
results.push({
scenario: scenario.name,
kositScenario: scenario.scenario,
schematronValid: validation?.schematronPassed || false,
schemaValid: validation?.schemaPassed || false,
businessRulesValid: validation?.businessRulesPassed || false,
overallValid: validation?.isValid || false
});
}
return results;
}
);
kositValidatorCompatibility.forEach(result => {
t.ok(result.overallValid, `KOSIT scenario ${result.scenario} should validate`);
});
// Test 8: Corpus XRechnung validation
const corpusXRechnungValidation = await performanceTracker.measureAsync(
'corpus-xrechnung-validation',
async () => {
const xrechnungFiles = await corpusLoader.getFilesByPattern('**/XRECHNUNG*.xml');
const results = {
total: xrechnungFiles.length,
valid: 0,
invalid: 0,
errors: [],
versions: {}
};
for (const file of xrechnungFiles.slice(0, 10)) { // Test first 10
try {
const content = await corpusLoader.readFile(file);
const parsed = await einvoice.parseDocument(content);
const validation = await einvoice.validateXRechnung(parsed);
if (validation?.isValid) {
results.valid++;
const version = validation.xrechnungVersion || 'unknown';
results.versions[version] = (results.versions[version] || 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(corpusXRechnungValidation.valid > 0, 'Some corpus files should be valid XRechnung');
// Test 9: German administrative requirements
const germanAdminRequirements = await performanceTracker.measureAsync(
'german-administrative-requirements',
async () => {
const adminTests = [
{
name: 'tax-number-format',
field: 'German tax number',
valid: 'DE123456789',
invalid: '123456789'
},
{
name: 'bank-account-iban',
field: 'IBAN',
valid: 'DE89370400440532013000',
invalid: 'DE00000000000000000000'
},
{
name: 'postal-code-format',
field: 'Postal code',
valid: '10115',
invalid: '1234'
},
{
name: 'email-format',
field: 'Email',
valid: 'rechnung@example.de',
invalid: 'invalid-email'
}
];
const results = [];
for (const test of adminTests) {
// Test valid format
const validInvoice = createXRechnungWithAdminField(test.field, test.valid);
const validResult = await einvoice.validateGermanAdminRequirements(validInvoice);
// Test invalid format
const invalidInvoice = createXRechnungWithAdminField(test.field, test.invalid);
const invalidResult = await einvoice.validateGermanAdminRequirements(invalidInvoice);
results.push({
requirement: test.name,
field: test.field,
validAccepted: validResult?.isValid || false,
invalidRejected: !invalidResult?.isValid,
formatError: invalidResult?.formatErrors?.[0]
});
}
return results;
}
);
germanAdminRequirements.forEach(result => {
t.ok(result.validAccepted, `Valid ${result.field} should be accepted`);
t.ok(result.invalidRejected, `Invalid ${result.field} should be rejected`);
});
// Test 10: XRechnung profile variations
const profileVariations = await performanceTracker.measureAsync(
'xrechnung-profile-variations',
async () => {
const profiles = [
{
name: 'B2G',
description: 'Business to Government',
requirements: ['Leitweg-ID', 'Buyer reference', 'Order reference']
},
{
name: 'B2B-public',
description: 'B2B with public sector involvement',
requirements: ['Buyer reference', 'Contract reference']
},
{
name: 'Cross-border',
description: 'Cross-border within EU',
requirements: ['VAT numbers', 'Country codes']
}
];
const results = [];
for (const profile of profiles) {
const invoice = createXRechnungForProfile(profile);
const validation = await einvoice.validateXRechnungProfile(invoice, profile.name);
results.push({
profile: profile.name,
description: profile.description,
valid: validation?.isValid || false,
profileCompliant: validation?.profileCompliant || false,
missingRequirements: validation?.missingRequirements || [],
additionalChecks: validation?.additionalChecksPassed || false
});
}
return results;
}
);
profileVariations.forEach(result => {
t.ok(result.profileCompliant, `XRechnung profile ${result.profile} should be compliant`);
});
// Print performance summary
performanceTracker.printSummary();
});
// Helper functions
function createCompleteXRechnungInvoice(): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<ubl:Invoice xmlns:ubl="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_2.3</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
<cbc:ID>RE-2024-00001</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>
<cbc:BuyerReference>04011000-12345-67</cbc:BuyerReference>
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="EM">seller@example.de</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>Verkäufer GmbH</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Musterstraße 1</cbc:StreetName>
<cbc:CityName>Berlin</cbc:CityName>
<cbc:PostalZone>10115</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>DE123456789</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="EM">buyer@example.de</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>Käufer AG</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Beispielweg 2</cbc:StreetName>
<cbc:CityName>Hamburg</cbc:CityName>
<cbc:PostalZone>20095</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:PaymentMeans>
<cbc:PaymentMeansCode>58</cbc:PaymentMeansCode>
<cac:PayeeFinancialAccount>
<cbc:ID>DE89370400440532013000</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">1190.00</cbc:TaxInclusiveAmount>
<cbc:PayableAmount currencyID="EUR">1190.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>Produkt A</cbc:Name>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</ubl:Invoice>`;
}
function createXRechnungWithoutField(fields: string | string[]): string {
const fieldsToRemove = Array.isArray(fields) ? fields : [fields];
let invoice = createCompleteXRechnungInvoice();
if (fieldsToRemove.includes('BT-10')) {
invoice = invoice.replace(/<cbc:BuyerReference>.*?<\/cbc:BuyerReference>/, '');
}
if (fieldsToRemove.includes('BT-49')) {
invoice = invoice.replace(/<cbc:EndpointID schemeID="EM">buyer@example.de<\/cbc:EndpointID>/, '');
}
return invoice;
}
function createXRechnungWithIncompleteAddress(): string {
let invoice = createCompleteXRechnungInvoice();
// Remove postal code from address
return invoice.replace(/<cbc:PostalZone>.*?<\/cbc:PostalZone>/, '');
}
function createInvoiceViolatingXRechnungRule(rule: string): string {
const base = createCompleteXRechnungInvoice();
switch (rule) {
case 'BR-DE-1':
// Remove payment account for credit transfer
return base.replace(/<cac:PayeeFinancialAccount>[\s\S]*?<\/cac:PayeeFinancialAccount>/, '');
case 'BR-DE-2':
// Remove buyer reference
return base.replace(/<cbc:BuyerReference>.*?<\/cbc:BuyerReference>/, '');
case 'BR-DE-3':
// Wrong specification identifier
return base.replace(
'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3',
'urn:cen.eu:en16931:2017'
);
default:
return base;
}
}
function createXRechnungWithLeitwegId(leitwegId: string | null): any {
return {
buyerReference: leitwegId,
supplierParty: { name: 'Test Supplier' },
customerParty: { name: 'Test Customer' }
};
}
function createXRechnungWithVersion(specId: string): string {
const base = createCompleteXRechnungInvoice();
return base.replace(
/<cbc:CustomizationID>.*?<\/cbc:CustomizationID>/,
`<cbc:CustomizationID>${specId}</cbc:CustomizationID>`
);
}
function createXRechnungWithCode(field: string, code: string): any {
return {
paymentMeansCode: field === 'Payment means' ? code : '58',
taxCategoryCode: field === 'Tax category' ? code : 'S',
invoiceTypeCode: field === 'Invoice type' ? code : '380'
};
}
function createXRechnungWithUBLExtension(): string {
const base = createCompleteXRechnungInvoice();
const extension = `
<ext:UBLExtensions xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2">
<ext:UBLExtension>
<ext:ExtensionContent>
<AdditionalData>Custom Value</AdditionalData>
</ext:ExtensionContent>
</ext:UBLExtension>
</ext:UBLExtensions>`;
return base.replace('<cbc:CustomizationID>', extension + '\n <cbc:CustomizationID>');
}
function createXRechnungWithAdditionalDocRef(): string {
const base = createCompleteXRechnungInvoice();
const docRef = `
<cac:AdditionalDocumentReference>
<cbc:ID>DOC-001</cbc:ID>
<cbc:DocumentType>Lieferschein</cbc:DocumentType>
</cac:AdditionalDocumentReference>`;
return base.replace('</ubl:Invoice>', docRef + '\n</ubl:Invoice>');
}
function createXRechnungWithCustomFields(): string {
const base = createCompleteXRechnungInvoice();
return base.replace(
'<cbc:Note>',
'<cbc:Note>CUSTOM:Field1=Value1;Field2=Value2</cbc:Note>\n <cbc:Note>'
);
}
function createInvoiceForKOSITScenario(scenario: string): string {
if (scenario === 'credit-note') {
return createCompleteXRechnungInvoice().replace(
'<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>',
'<cbc:InvoiceTypeCode>381</cbc:InvoiceTypeCode>'
);
}
return createCompleteXRechnungInvoice();
}
function createXRechnungWithAdminField(field: string, value: string): any {
const invoice = {
supplierTaxId: field === 'German tax number' ? value : 'DE123456789',
paymentAccount: field === 'IBAN' ? value : 'DE89370400440532013000',
postalCode: field === 'Postal code' ? value : '10115',
email: field === 'Email' ? value : 'test@example.de'
};
return invoice;
}
function createXRechnungForProfile(profile: any): string {
const base = createCompleteXRechnungInvoice();
if (profile.name === 'B2G') {
// Already has Leitweg-ID as BuyerReference
return base;
} else if (profile.name === 'Cross-border') {
// Add foreign VAT number
return base.replace(
'<cbc:CompanyID>DE123456789</cbc:CompanyID>',
'<cbc:CompanyID>ATU12345678</cbc:CompanyID>'
);
}
return base;
}
const leitwegTests = [
{ name: 'valid-format', leitwegId: '04011000-12345-67', expected: { valid: true } },
{ name: 'valid-with-extension', leitwegId: '04011000-12345-67-001', expected: { valid: true } },
{ name: 'invalid-checksum', leitwegId: '04011000-12345-99', expected: { valid: false } },
{ name: 'invalid-format', leitwegId: '12345', expected: { valid: false } },
{ name: 'missing-leitweg', leitwegId: null, expected: { valid: false } }
];
const versions = [
{ version: '1.2', specId: 'urn:cen.eu:en16931:2017:compliant:xoev-de:kosit:standard:xrechnung_1.2', supported: false },
{ version: '2.0', specId: 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0', supported: true },
{ version: '2.3', specId: 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3', supported: true },
{ version: '3.0', specId: 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_3.0', supported: true }
];
// Run the test
tap.start();