792 lines
26 KiB
TypeScript
792 lines
26 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-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();
|