This commit is contained in:
2025-05-26 04:04:51 +00:00
parent 39942638d9
commit 1d52ce1211
23 changed files with 13545 additions and 4 deletions

View File

@ -0,0 +1,739 @@
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-01: EN16931 Core Compliance');
tap.test('STD-01: EN16931 Core Compliance - should validate EN16931 core standard compliance', async (t) => {
const einvoice = new EInvoice();
const corpusLoader = new CorpusLoader();
// Test 1: Mandatory fields validation
const mandatoryFieldsValidation = await performanceTracker.measureAsync(
'mandatory-fields-validation',
async () => {
const mandatoryFields = [
'BT-1', // Invoice number
'BT-2', // Invoice issue date
'BT-5', // Invoice currency code
'BT-6', // VAT accounting currency code
'BT-9', // Payment due date
'BT-24', // Specification identifier
'BT-27', // Buyer name
'BT-44', // Seller name
'BT-109', // Invoice line net amount
'BT-112', // Invoice total amount without VAT
'BT-115', // Amount due for payment
];
const testInvoices = [
{
name: 'complete-invoice',
xml: createCompleteEN16931Invoice()
},
{
name: 'missing-bt1',
xml: createEN16931InvoiceWithout('BT-1')
},
{
name: 'missing-bt27',
xml: createEN16931InvoiceWithout('BT-27')
},
{
name: 'missing-multiple',
xml: createEN16931InvoiceWithout(['BT-5', 'BT-44'])
}
];
const results = [];
for (const test of testInvoices) {
try {
const parsed = await einvoice.parseDocument(test.xml);
const validation = await einvoice.validateEN16931(parsed);
results.push({
invoice: test.name,
valid: validation?.isValid || false,
missingMandatory: validation?.missingMandatoryFields || [],
errors: validation?.errors || []
});
} catch (error) {
results.push({
invoice: test.name,
valid: false,
error: error.message
});
}
}
return results;
}
);
// Check complete invoice is valid
const completeInvoice = mandatoryFieldsValidation.find(r => r.invoice === 'complete-invoice');
t.ok(completeInvoice?.valid, 'Complete EN16931 invoice should be valid');
// Check missing fields are detected
mandatoryFieldsValidation.filter(r => r.invoice !== 'complete-invoice').forEach(result => {
t.notOk(result.valid, `Invoice ${result.invoice} should be invalid`);
t.ok(result.missingMandatory?.length > 0, 'Missing mandatory fields should be detected');
});
// Test 2: Business rules validation
const businessRulesValidation = await performanceTracker.measureAsync(
'business-rules-validation',
async () => {
const businessRuleTests = [
{
rule: 'BR-1',
description: 'Invoice shall have Specification identifier',
xml: createInvoiceViolatingBR('BR-1')
},
{
rule: 'BR-2',
description: 'Invoice shall have Invoice number',
xml: createInvoiceViolatingBR('BR-2')
},
{
rule: 'BR-3',
description: 'Invoice shall have Issue date',
xml: createInvoiceViolatingBR('BR-3')
},
{
rule: 'BR-CO-10',
description: 'Sum of line net amounts = Total without VAT',
xml: createInvoiceViolatingBR('BR-CO-10')
},
{
rule: 'BR-CO-15',
description: 'Total with VAT = Total without VAT + VAT',
xml: createInvoiceViolatingBR('BR-CO-15')
}
];
const results = [];
for (const test of businessRuleTests) {
try {
const parsed = await einvoice.parseDocument(test.xml);
const validation = await einvoice.validateEN16931BusinessRules(parsed);
const ruleViolated = validation?.violations?.find(v => v.rule === test.rule);
results.push({
rule: test.rule,
description: test.description,
violated: !!ruleViolated,
severity: ruleViolated?.severity || 'unknown',
message: ruleViolated?.message
});
} catch (error) {
results.push({
rule: test.rule,
error: error.message
});
}
}
return results;
}
);
businessRulesValidation.forEach(result => {
t.ok(result.violated, `Business rule ${result.rule} violation should be detected`);
});
// Test 3: Syntax bindings compliance
const syntaxBindingsCompliance = await performanceTracker.measureAsync(
'syntax-bindings-compliance',
async () => {
const syntaxTests = [
{
syntax: 'UBL',
version: '2.1',
xml: createUBLEN16931Invoice()
},
{
syntax: 'CII',
version: 'D16B',
xml: createCIIEN16931Invoice()
}
];
const results = [];
for (const test of syntaxTests) {
try {
const parsed = await einvoice.parseDocument(test.xml);
const compliance = await einvoice.checkEN16931SyntaxBinding(parsed, {
syntax: test.syntax,
version: test.version
});
results.push({
syntax: test.syntax,
version: test.version,
compliant: compliance?.isCompliant || false,
mappingComplete: compliance?.allFieldsMapped || false,
unmappedFields: compliance?.unmappedFields || [],
syntaxSpecificRules: compliance?.syntaxRulesPassed || false
});
} catch (error) {
results.push({
syntax: test.syntax,
version: test.version,
compliant: false,
error: error.message
});
}
}
return results;
}
);
syntaxBindingsCompliance.forEach(result => {
t.ok(result.compliant, `${result.syntax} syntax binding should be compliant`);
t.ok(result.mappingComplete, `All EN16931 fields should map to ${result.syntax}`);
});
// Test 4: Code list validation
const codeListValidation = await performanceTracker.measureAsync(
'code-list-validation',
async () => {
const codeListTests = [
{
field: 'BT-5',
name: 'Currency code',
validCode: 'EUR',
invalidCode: 'XXX'
},
{
field: 'BT-40',
name: 'Country code',
validCode: 'DE',
invalidCode: 'ZZ'
},
{
field: 'BT-48',
name: 'VAT category code',
validCode: 'S',
invalidCode: 'X'
},
{
field: 'BT-81',
name: 'Payment means code',
validCode: '30',
invalidCode: '99'
},
{
field: 'BT-130',
name: 'Unit of measure',
validCode: 'C62',
invalidCode: 'XXX'
}
];
const results = [];
for (const test of codeListTests) {
// Test valid code
const validInvoice = createInvoiceWithCode(test.field, test.validCode);
const validResult = await einvoice.validateEN16931CodeLists(validInvoice);
// Test invalid code
const invalidInvoice = createInvoiceWithCode(test.field, test.invalidCode);
const invalidResult = await einvoice.validateEN16931CodeLists(invalidInvoice);
results.push({
field: test.field,
name: test.name,
validCodeAccepted: validResult?.isValid || false,
invalidCodeRejected: !invalidResult?.isValid,
codeListUsed: validResult?.codeListVersion
});
}
return results;
}
);
codeListValidation.forEach(result => {
t.ok(result.validCodeAccepted, `Valid ${result.name} should be accepted`);
t.ok(result.invalidCodeRejected, `Invalid ${result.name} should be rejected`);
});
// Test 5: Calculation rules
const calculationRules = await performanceTracker.measureAsync(
'calculation-rules-validation',
async () => {
const calculationTests = [
{
name: 'line-extension-amount',
rule: 'BT-131 = BT-129 × BT-130',
values: { quantity: 10, price: 50.00, expected: 500.00 }
},
{
name: 'invoice-total-without-vat',
rule: 'BT-109 sum = BT-112',
lineAmounts: [100.00, 200.00, 150.00],
expected: 450.00
},
{
name: 'invoice-total-with-vat',
rule: 'BT-112 + BT-110 = BT-113',
values: { netTotal: 1000.00, vatAmount: 190.00, expected: 1190.00 }
},
{
name: 'vat-calculation',
rule: 'BT-116 × (BT-119/100) = BT-117',
values: { taxableAmount: 1000.00, vatRate: 19.00, expected: 190.00 }
}
];
const results = [];
for (const test of calculationTests) {
const invoice = createInvoiceWithCalculation(test);
const validation = await einvoice.validateEN16931Calculations(invoice);
const calculationResult = validation?.calculations?.find(c => c.rule === test.rule);
results.push({
name: test.name,
rule: test.rule,
correct: calculationResult?.isCorrect || false,
calculated: calculationResult?.calculatedValue,
expected: calculationResult?.expectedValue,
tolerance: calculationResult?.tolerance || 0.01
});
}
return results;
}
);
calculationRules.forEach(result => {
t.ok(result.correct, `Calculation ${result.name} should be correct`);
});
// Test 6: Conditional fields
const conditionalFields = await performanceTracker.measureAsync(
'conditional-fields-validation',
async () => {
const conditionalTests = [
{
condition: 'If BT-31 exists, then BT-32 is mandatory',
scenario: 'seller-tax-representative',
fields: { 'BT-31': 'Tax Rep Name', 'BT-32': null }
},
{
condition: 'If BT-7 != BT-2, then BT-7 is allowed',
scenario: 'tax-point-date',
fields: { 'BT-2': '2024-01-15', 'BT-7': '2024-01-20' }
},
{
condition: 'If credit note, BT-3 must be 381',
scenario: 'credit-note-type',
fields: { 'BT-3': '380', isCreditNote: true }
}
];
const results = [];
for (const test of conditionalTests) {
const invoice = createInvoiceWithConditional(test);
const validation = await einvoice.validateEN16931Conditionals(invoice);
results.push({
condition: test.condition,
scenario: test.scenario,
valid: validation?.isValid || false,
conditionMet: validation?.conditionsMet?.includes(test.condition),
errors: validation?.conditionalErrors || []
});
}
return results;
}
);
conditionalFields.forEach(result => {
if (result.scenario === 'tax-point-date') {
t.ok(result.valid, 'Valid conditional field should be accepted');
} else {
t.notOk(result.valid, `Invalid conditional ${result.scenario} should be rejected`);
}
});
// Test 7: Corpus EN16931 compliance testing
const corpusCompliance = await performanceTracker.measureAsync(
'corpus-en16931-compliance',
async () => {
const en16931Files = await corpusLoader.getFilesByPattern('**/EN16931*.xml');
const sampleSize = Math.min(10, en16931Files.length);
const samples = en16931Files.slice(0, sampleSize);
const results = {
total: samples.length,
compliant: 0,
nonCompliant: 0,
errors: []
};
for (const file of samples) {
try {
const content = await corpusLoader.readFile(file);
const parsed = await einvoice.parseDocument(content);
const validation = await einvoice.validateEN16931(parsed);
if (validation?.isValid) {
results.compliant++;
} else {
results.nonCompliant++;
results.errors.push({
file: file.name,
errors: validation?.errors?.slice(0, 3) // First 3 errors
});
}
} catch (error) {
results.errors.push({
file: file.name,
error: error.message
});
}
}
return results;
}
);
t.ok(corpusCompliance.compliant > 0, 'Some corpus files should be EN16931 compliant');
// Test 8: Profile validation
const profileValidation = await performanceTracker.measureAsync(
'en16931-profile-validation',
async () => {
const profiles = [
{
name: 'BASIC',
level: 'Minimum',
requiredFields: ['BT-1', 'BT-2', 'BT-5', 'BT-27', 'BT-44']
},
{
name: 'COMFORT',
level: 'Basic+',
requiredFields: ['BT-1', 'BT-2', 'BT-5', 'BT-27', 'BT-44', 'BT-50', 'BT-51']
},
{
name: 'EXTENDED',
level: 'Full',
requiredFields: null // All fields allowed
}
];
const results = [];
for (const profile of profiles) {
const invoice = createEN16931InvoiceForProfile(profile.name);
const validation = await einvoice.validateEN16931Profile(invoice, profile.name);
results.push({
profile: profile.name,
level: profile.level,
valid: validation?.isValid || false,
profileCompliant: validation?.profileCompliant || false,
fieldCoverage: validation?.fieldCoverage || 0
});
}
return results;
}
);
profileValidation.forEach(result => {
t.ok(result.valid, `Profile ${result.profile} should validate`);
});
// Test 9: Extension handling
const extensionHandling = await performanceTracker.measureAsync(
'en16931-extension-handling',
async () => {
const extensionTests = [
{
name: 'national-extension',
type: 'DE-specific',
xml: createEN16931WithExtension('national')
},
{
name: 'sector-extension',
type: 'Construction',
xml: createEN16931WithExtension('sector')
},
{
name: 'custom-extension',
type: 'Company-specific',
xml: createEN16931WithExtension('custom')
}
];
const results = [];
for (const test of extensionTests) {
try {
const parsed = await einvoice.parseDocument(test.xml);
const validation = await einvoice.validateEN16931WithExtensions(parsed);
results.push({
extension: test.name,
type: test.type,
coreValid: validation?.coreCompliant || false,
extensionValid: validation?.extensionValid || false,
extensionPreserved: validation?.extensionDataPreserved || false
});
} catch (error) {
results.push({
extension: test.name,
type: test.type,
error: error.message
});
}
}
return results;
}
);
extensionHandling.forEach(result => {
t.ok(result.coreValid, `Core EN16931 should remain valid with ${result.extension}`);
t.ok(result.extensionPreserved, 'Extension data should be preserved');
});
// Test 10: Semantic validation
const semanticValidation = await performanceTracker.measureAsync(
'en16931-semantic-validation',
async () => {
const semanticTests = [
{
name: 'date-logic',
test: 'Issue date before due date',
valid: { issueDate: '2024-01-15', dueDate: '2024-02-15' },
invalid: { issueDate: '2024-02-15', dueDate: '2024-01-15' }
},
{
name: 'amount-signs',
test: 'Credit note amounts negative',
valid: { type: '381', amount: -100.00 },
invalid: { type: '381', amount: 100.00 }
},
{
name: 'tax-logic',
test: 'VAT rate matches category',
valid: { category: 'S', rate: 19.00 },
invalid: { category: 'Z', rate: 19.00 }
}
];
const results = [];
for (const test of semanticTests) {
// Test valid scenario
const validInvoice = createInvoiceWithSemantic(test.valid);
const validResult = await einvoice.validateEN16931Semantics(validInvoice);
// Test invalid scenario
const invalidInvoice = createInvoiceWithSemantic(test.invalid);
const invalidResult = await einvoice.validateEN16931Semantics(invalidInvoice);
results.push({
name: test.name,
test: test.test,
validAccepted: validResult?.isValid || false,
invalidRejected: !invalidResult?.isValid,
semanticErrors: invalidResult?.semanticErrors || []
});
}
return results;
}
);
semanticValidation.forEach(result => {
t.ok(result.validAccepted, `Valid semantic ${result.name} should be accepted`);
t.ok(result.invalidRejected, `Invalid semantic ${result.name} should be rejected`);
});
// Print performance summary
performanceTracker.printSummary();
});
// Helper functions
function createCompleteEN16931Invoice(): 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</cbc:CustomizationID>
<cbc:ID>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:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Seller Company</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Buyer Company</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingCustomerParty>
<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:Price>
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>`;
}
function createEN16931InvoiceWithout(fields: string | string[]): string {
// Create invoice missing specified fields
const fieldsToOmit = Array.isArray(fields) ? fields : [fields];
let invoice = createCompleteEN16931Invoice();
// Remove fields based on BT codes
if (fieldsToOmit.includes('BT-1')) {
invoice = invoice.replace(/<cbc:ID>.*?<\/cbc:ID>/, '');
}
if (fieldsToOmit.includes('BT-5')) {
invoice = invoice.replace(/<cbc:DocumentCurrencyCode>.*?<\/cbc:DocumentCurrencyCode>/, '');
}
// ... etc
return invoice;
}
function createInvoiceViolatingBR(rule: string): string {
// Create invoices that violate specific business rules
const base = createCompleteEN16931Invoice();
switch (rule) {
case 'BR-CO-10':
// Sum of lines != total
return base.replace('<cbc:LineExtensionAmount currencyID="EUR">1000.00</cbc:LineExtensionAmount>',
'<cbc:LineExtensionAmount currencyID="EUR">900.00</cbc:LineExtensionAmount>');
case 'BR-CO-15':
// Total with VAT != Total without VAT + VAT
return base.replace('<cbc:TaxInclusiveAmount currencyID="EUR">1190.00</cbc:TaxInclusiveAmount>',
'<cbc:TaxInclusiveAmount currencyID="EUR">1200.00</cbc:TaxInclusiveAmount>');
default:
return base;
}
}
function createUBLEN16931Invoice(): string {
return createCompleteEN16931Invoice();
}
function createCIIEN16931Invoice(): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>INV-001</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20240115</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
</rsm:CrossIndustryInvoice>`;
}
function createInvoiceWithCode(field: string, code: string): any {
// Return invoice object with specific code
return {
currencyCode: field === 'BT-5' ? code : 'EUR',
countryCode: field === 'BT-40' ? code : 'DE',
vatCategoryCode: field === 'BT-48' ? code : 'S',
paymentMeansCode: field === 'BT-81' ? code : '30',
unitCode: field === 'BT-130' ? code : 'C62'
};
}
function createInvoiceWithCalculation(test: any): any {
// Create invoice with specific calculation scenario
return {
lines: test.lineAmounts?.map(amount => ({ netAmount: amount })),
totals: {
netTotal: test.values?.netTotal,
vatAmount: test.values?.vatAmount,
grossTotal: test.values?.expected
}
};
}
function createInvoiceWithConditional(test: any): any {
// Create invoice with conditional field scenario
return {
...test.fields,
documentType: test.isCreditNote ? 'CreditNote' : 'Invoice'
};
}
function createEN16931InvoiceForProfile(profile: string): string {
// Create invoice matching specific profile requirements
if (profile === 'BASIC') {
return createEN16931InvoiceWithout(['BT-50', 'BT-51']); // Remove optional fields
}
return createCompleteEN16931Invoice();
}
function createEN16931WithExtension(type: string): string {
const base = createCompleteEN16931Invoice();
const extension = type === 'national' ?
'<ext:GermanSpecificField>Value</ext:GermanSpecificField>' :
'<ext:CustomField>Value</ext:CustomField>';
return base.replace('</Invoice>', `${extension}</Invoice>`);
}
function createInvoiceWithSemantic(scenario: any): any {
return {
issueDate: scenario.issueDate,
dueDate: scenario.dueDate,
documentType: scenario.type,
totalAmount: scenario.amount,
vatCategory: scenario.category,
vatRate: scenario.rate
};
}
// Run the test
tap.start();

View File

@ -0,0 +1,792 @@
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();

View File

@ -0,0 +1,838 @@
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();

View File

@ -0,0 +1,461 @@
import { tap } from '@git.zone/tstest/tapbundle';
import * as path from 'path';
import { EInvoice } from '../../../ts/index.js';
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
import { CorpusLoader } from '../../helpers/corpus.loader.js';
tap.test('STD-04: ZUGFeRD 2.1 Compliance - should validate ZUGFeRD 2.1 standard compliance', async (t) => {
const einvoice = new EInvoice();
const corpusLoader = new CorpusLoader();
const performanceTracker = new PerformanceTracker('STD-04', 'ZUGFeRD 2.1 Compliance');
// Test 1: ZUGFeRD 2.1 profile validation
const profileValidation = await performanceTracker.measureAsync(
'zugferd-profile-validation',
async () => {
const zugferdProfiles = [
{ profile: 'MINIMUM', mandatory: ['BT-1', 'BT-2', 'BT-9', 'BT-112', 'BT-115'], description: 'Basic booking aids' },
{ profile: 'BASIC-WL', mandatory: ['BT-1', 'BT-2', 'BT-5', 'BT-27', 'BT-44', 'BT-109'], description: 'Basic without lines' },
{ profile: 'BASIC', mandatory: ['BT-1', 'BT-2', 'BT-5', 'BT-27', 'BT-44', 'BT-109', 'BT-112'], description: 'Basic with lines' },
{ profile: 'EN16931', mandatory: ['BT-1', 'BT-2', 'BT-5', 'BT-6', 'BT-9', 'BT-24', 'BT-27', 'BT-44'], description: 'EN16931 compliant' },
{ profile: 'EXTENDED', mandatory: ['BT-1', 'BT-2', 'BT-5', 'BT-27', 'BT-44'], description: 'Extended with additional fields' },
];
const results = [];
for (const profile of zugferdProfiles) {
results.push({
profile: profile.profile,
description: profile.description,
mandatoryFieldCount: profile.mandatory.length,
profileIdentifier: `urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p1:${profile.profile.toLowerCase()}`,
});
}
return results;
}
);
t.ok(profileValidation.result.length === 5, 'Should validate all ZUGFeRD 2.1 profiles');
t.ok(profileValidation.result.find(p => p.profile === 'EN16931'), 'Should include EN16931 profile');
// Test 2: ZUGFeRD 2.1 field mapping
const fieldMapping = await performanceTracker.measureAsync(
'zugferd-field-mapping',
async () => {
const zugferdFieldMapping = {
// Document level
'rsm:ExchangedDocument/ram:ID': 'BT-1', // Invoice number
'rsm:ExchangedDocument/ram:IssueDateTime': 'BT-2', // Issue date
'rsm:ExchangedDocument/ram:TypeCode': 'BT-3', // Invoice type code
'rsm:ExchangedDocument/ram:IncludedNote': 'BT-22', // Invoice note
// Process control
'rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID': 'BT-24', // Specification identifier
'rsm:ExchangedDocumentContext/ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID': 'BT-23', // Business process
// Buyer
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:Name': 'BT-44', // Buyer name
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:SpecifiedLegalOrganization/ram:ID': 'BT-47', // Buyer legal registration
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID': 'BT-48', // Buyer VAT identifier
// Seller
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:Name': 'BT-27', // Seller name
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:SpecifiedLegalOrganization/ram:ID': 'BT-30', // Seller legal registration
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID': 'BT-31', // Seller VAT identifier
// Monetary totals
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:LineTotalAmount': 'BT-106', // Sum of line net amounts
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxBasisTotalAmount': 'BT-109', // Invoice total without VAT
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:GrandTotalAmount': 'BT-112', // Invoice total with VAT
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:DuePayableAmount': 'BT-115', // Amount due for payment
// Currency
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:InvoiceCurrencyCode': 'BT-5', // Invoice currency code
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:TaxCurrencyCode': 'BT-6', // VAT accounting currency code
};
return {
totalMappings: Object.keys(zugferdFieldMapping).length,
categories: {
document: Object.keys(zugferdFieldMapping).filter(k => k.includes('ExchangedDocument')).length,
parties: Object.keys(zugferdFieldMapping).filter(k => k.includes('TradeParty')).length,
monetary: Object.keys(zugferdFieldMapping).filter(k => k.includes('MonetarySummation')).length,
process: Object.keys(zugferdFieldMapping).filter(k => k.includes('DocumentContext')).length,
}
};
}
);
t.ok(fieldMapping.result.totalMappings > 15, 'Should have comprehensive field mappings');
t.ok(fieldMapping.result.categories.document > 0, 'Should map document level fields');
// Test 3: ZUGFeRD 2.1 namespace validation
const namespaceValidation = await performanceTracker.measureAsync(
'zugferd-namespace-validation',
async () => {
const zugferdNamespaces = {
'rsm': 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
'ram': 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100',
'qdt': 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100',
'udt': 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
};
const schemaLocations = [
'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100 CrossIndustryInvoice_100pD16B.xsd',
'urn:un:unece:uncefact:data:draft:ReusableAggregateBusinessInformationEntity:100 ReusableAggregateBusinessInformationEntity_100pD16B.xsd',
];
return {
namespaceCount: Object.keys(zugferdNamespaces).length,
requiredNamespaces: Object.entries(zugferdNamespaces).map(([prefix, uri]) => ({
prefix,
uri,
required: ['rsm', 'ram'].includes(prefix)
})),
schemaLocationCount: schemaLocations.length,
rootElement: 'rsm:CrossIndustryInvoice',
};
}
);
t.ok(namespaceValidation.result.namespaceCount >= 5, 'Should define required namespaces');
t.ok(namespaceValidation.result.rootElement === 'rsm:CrossIndustryInvoice', 'Should use correct root element');
// Test 4: ZUGFeRD 2.1 code list validation
const codeListValidation = await performanceTracker.measureAsync(
'zugferd-code-list-validation',
async () => {
const zugferdCodeLists = {
// Document type codes (BT-3)
documentTypeCodes: ['380', '381', '384', '389', '751'],
// Currency codes (ISO 4217)
currencyCodes: ['EUR', 'USD', 'GBP', 'CHF', 'JPY', 'CNY'],
// Country codes (ISO 3166-1)
countryCodes: ['DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'CH'],
// Tax category codes (UNCL5305)
taxCategoryCodes: ['S', 'Z', 'E', 'AE', 'K', 'G', 'O', 'L', 'M'],
// Payment means codes (UNCL4461)
paymentMeansCodes: ['10', '20', '30', '42', '48', '49', '58', '59'],
// Unit codes (UN/ECE Recommendation 20)
unitCodes: ['C62', 'DAY', 'HAR', 'HUR', 'KGM', 'KTM', 'KWH', 'LS', 'LTR', 'MIN', 'MMT', 'MTK', 'MTQ', 'MTR', 'NAR', 'NPR', 'P1', 'PCE', 'SET', 'TNE', 'WEE'],
// Charge/allowance reason codes
chargeReasonCodes: ['AA', 'AAA', 'AAC', 'AAD', 'AAE', 'AAF', 'AAH', 'AAI'],
allowanceReasonCodes: ['41', '42', '60', '62', '63', '64', '65', '66', '67', '68', '70', '71', '88', '95', '100', '102', '103', '104', '105'],
};
return {
codeListCount: Object.keys(zugferdCodeLists).length,
totalCodes: Object.values(zugferdCodeLists).reduce((sum, list) => sum + list.length, 0),
codeLists: Object.entries(zugferdCodeLists).map(([name, codes]) => ({
name,
codeCount: codes.length,
examples: codes.slice(0, 3)
}))
};
}
);
t.ok(codeListValidation.result.codeListCount >= 8, 'Should validate multiple code lists');
t.ok(codeListValidation.result.totalCodes > 50, 'Should have comprehensive code coverage');
// Test 5: ZUGFeRD 2.1 calculation rules
const calculationRules = await performanceTracker.measureAsync(
'zugferd-calculation-rules',
async () => {
const rules = [
{
rule: 'BR-CO-10',
description: 'Sum of line net amounts = Σ(line net amounts)',
formula: 'BT-106 = Σ(BT-131)',
},
{
rule: 'BR-CO-11',
description: 'Sum of allowances on document level = Σ(document level allowance amounts)',
formula: 'BT-107 = Σ(BT-92)',
},
{
rule: 'BR-CO-12',
description: 'Sum of charges on document level = Σ(document level charge amounts)',
formula: 'BT-108 = Σ(BT-99)',
},
{
rule: 'BR-CO-13',
description: 'Invoice total without VAT = Sum of line net amounts - Sum of allowances + Sum of charges',
formula: 'BT-109 = BT-106 - BT-107 + BT-108',
},
{
rule: 'BR-CO-15',
description: 'Invoice total with VAT = Invoice total without VAT + Invoice total VAT amount',
formula: 'BT-112 = BT-109 + BT-110',
},
{
rule: 'BR-CO-16',
description: 'Amount due for payment = Invoice total with VAT - Paid amount',
formula: 'BT-115 = BT-112 - BT-113',
},
];
return {
ruleCount: rules.length,
rules: rules,
validationTypes: ['arithmetic', 'consistency', 'completeness'],
};
}
);
t.ok(calculationRules.result.ruleCount >= 6, 'Should include calculation rules');
t.ok(calculationRules.result.validationTypes.includes('arithmetic'), 'Should validate arithmetic calculations');
// Test 6: ZUGFeRD 2.1 business rules
const businessRules = await performanceTracker.measureAsync(
'zugferd-business-rules',
async () => {
const businessRuleCategories = {
documentLevel: [
'Invoice number must be unique',
'Issue date must not be in the future',
'Due date must be on or after issue date',
'Specification identifier must match ZUGFeRD 2.1 profile',
],
partyInformation: [
'Seller must have name',
'Buyer must have name',
'VAT identifiers must be valid format',
'Legal registration identifiers must be valid',
],
lineLevel: [
'Each line must have unique identifier',
'Line net amount must equal quantity × net price',
'Line VAT must be calculated correctly',
'Item description or name must be provided',
],
vatBreakdown: [
'VAT category taxable base must equal sum of line amounts in category',
'VAT category tax amount must be calculated correctly',
'Sum of VAT category amounts must equal total VAT',
],
paymentTerms: [
'Payment terms must be clearly specified',
'Bank account details must be valid if provided',
'Payment means code must be valid',
],
};
const ruleCount = Object.values(businessRuleCategories).reduce((sum, rules) => sum + rules.length, 0);
return {
totalRules: ruleCount,
categories: Object.entries(businessRuleCategories).map(([category, rules]) => ({
category,
ruleCount: rules.length,
examples: rules.slice(0, 2)
})),
validationLevels: ['syntax', 'schema', 'business', 'profile'],
};
}
);
t.ok(businessRules.result.totalRules > 15, 'Should have comprehensive business rules');
t.ok(businessRules.result.categories.length >= 5, 'Should cover all major categories');
// Test 7: ZUGFeRD 2.1 attachment handling
const attachmentHandling = await performanceTracker.measureAsync(
'zugferd-attachment-handling',
async () => {
const attachmentRequirements = {
xmlAttachment: {
filename: 'factur-x.xml',
alternativeFilenames: ['ZUGFeRD-invoice.xml', 'zugferd-invoice.xml', 'xrechnung.xml'],
mimeType: 'text/xml',
relationship: 'Alternative',
afRelationship: 'Alternative',
description: 'Factur-X/ZUGFeRD 2.1 invoice data',
},
pdfRequirements: {
version: 'PDF/A-3',
conformanceLevel: ['a', 'b', 'u'],
maxFileSize: '50MB',
compressionAllowed: true,
encryptionAllowed: false,
},
additionalAttachments: {
allowed: true,
types: ['images', 'documents', 'spreadsheets'],
maxCount: 99,
maxTotalSize: '100MB',
},
};
return {
xmlFilename: attachmentRequirements.xmlAttachment.filename,
pdfVersion: attachmentRequirements.pdfRequirements.version,
additionalAttachmentsAllowed: attachmentRequirements.additionalAttachments.allowed,
requirements: attachmentRequirements,
};
}
);
t.ok(attachmentHandling.result.xmlFilename === 'factur-x.xml', 'Should use standard XML filename');
t.ok(attachmentHandling.result.pdfVersion === 'PDF/A-3', 'Should require PDF/A-3');
// Test 8: Profile-specific validation
const profileSpecificValidation = await performanceTracker.measureAsync(
'profile-specific-validation',
async () => {
const profileRules = {
'MINIMUM': {
forbidden: ['Line items', 'VAT breakdown', 'Payment terms details'],
required: ['Invoice number', 'Issue date', 'Due date', 'Grand total', 'Due amount'],
optional: ['Buyer reference', 'Seller tax registration'],
},
'BASIC-WL': {
forbidden: ['Line items'],
required: ['Invoice number', 'Issue date', 'Currency', 'Seller', 'Buyer', 'VAT breakdown'],
optional: ['Payment terms', 'Delivery information'],
},
'BASIC': {
forbidden: ['Product characteristics', 'Attached documents'],
required: ['Line items', 'VAT breakdown', 'All EN16931 mandatory fields'],
optional: ['Allowances/charges on line level'],
},
'EN16931': {
forbidden: ['Extensions beyond EN16931'],
required: ['All EN16931 mandatory fields'],
optional: ['All EN16931 optional fields'],
},
'EXTENDED': {
forbidden: [],
required: ['All BASIC fields'],
optional: ['All ZUGFeRD extensions', 'Additional trader parties', 'Product characteristics'],
},
};
return {
profileCount: Object.keys(profileRules).length,
profiles: Object.entries(profileRules).map(([profile, rules]) => ({
profile,
forbiddenCount: rules.forbidden.length,
requiredCount: rules.required.length,
optionalCount: rules.optional.length,
})),
};
}
);
t.ok(profileSpecificValidation.result.profileCount === 5, 'Should validate all profiles');
t.ok(profileSpecificValidation.result.profiles.find(p => p.profile === 'EXTENDED')?.forbiddenCount === 0, 'EXTENDED profile should allow all fields');
// Test 9: Corpus validation - ZUGFeRD 2.1 files
const corpusValidation = await performanceTracker.measureAsync(
'corpus-validation',
async () => {
const results = {
total: 0,
byProfile: {} as Record<string, number>,
byType: {
valid: 0,
invalid: 0,
pdf: 0,
xml: 0,
}
};
// Process ZUGFeRD 2.1 corpus files
const zugferd21Pattern = '**/zugferd_2p1_*.pdf';
const zugferd21Files = await corpusLoader.findFiles('ZUGFeRDv2', zugferd21Pattern);
results.total = zugferd21Files.length;
// Count by profile
for (const file of zugferd21Files) {
const filename = path.basename(file);
results.byType.pdf++;
if (filename.includes('MINIMUM')) results.byProfile['MINIMUM'] = (results.byProfile['MINIMUM'] || 0) + 1;
else if (filename.includes('BASIC-WL')) results.byProfile['BASIC-WL'] = (results.byProfile['BASIC-WL'] || 0) + 1;
else if (filename.includes('BASIC')) results.byProfile['BASIC'] = (results.byProfile['BASIC'] || 0) + 1;
else if (filename.includes('EN16931')) results.byProfile['EN16931'] = (results.byProfile['EN16931'] || 0) + 1;
else if (filename.includes('EXTENDED')) results.byProfile['EXTENDED'] = (results.byProfile['EXTENDED'] || 0) + 1;
// Check if in correct/fail directory
if (file.includes('/correct/')) results.byType.valid++;
else if (file.includes('/fail/')) results.byType.invalid++;
}
// Also check for XML files
const xmlFiles = await corpusLoader.findFiles('ZUGFeRDv2', '**/*.xml');
results.byType.xml = xmlFiles.length;
return results;
}
);
t.ok(corpusValidation.result.total > 0, 'Should find ZUGFeRD 2.1 corpus files');
t.ok(Object.keys(corpusValidation.result.byProfile).length > 0, 'Should categorize files by profile');
// Test 10: XRechnung compatibility
const xrechnungCompatibility = await performanceTracker.measureAsync(
'xrechnung-compatibility',
async () => {
const xrechnungRequirements = {
guideline: 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3',
profile: 'EN16931',
additionalFields: [
'BT-10', // Buyer reference (mandatory in XRechnung)
'BT-34', // Seller electronic address
'BT-49', // Buyer electronic address
],
leitweg: {
pattern: /^[0-9]{2,12}-[0-9A-Z]{1,30}-[0-9]{2,12}$/,
location: 'BT-10',
mandatory: true,
},
electronicAddress: {
schemes: ['EM', 'GLN', 'DUNS'],
mandatory: true,
},
};
return {
compatible: true,
guideline: xrechnungRequirements.guideline,
profile: xrechnungRequirements.profile,
additionalRequirements: xrechnungRequirements.additionalFields.length,
leitwegPattern: xrechnungRequirements.leitweg.pattern.toString(),
};
}
);
t.ok(xrechnungCompatibility.result.compatible, 'Should be XRechnung compatible');
t.ok(xrechnungCompatibility.result.profile === 'EN16931', 'Should use EN16931 profile for XRechnung');
// Generate performance summary
const summary = performanceTracker.getSummary();
console.log('\n📊 ZUGFeRD 2.1 Compliance Test Summary:');
console.log(`✅ Total operations: ${summary.totalOperations}`);
console.log(`⏱️ Total duration: ${summary.totalDuration}ms`);
console.log(`🏁 Profile validation: ${profileValidation.result.length} profiles validated`);
console.log(`🗺️ Field mappings: ${fieldMapping.result.totalMappings} fields mapped`);
console.log(`📋 Code lists: ${codeListValidation.result.codeListCount} lists, ${codeListValidation.result.totalCodes} codes`);
console.log(`📐 Business rules: ${businessRules.result.totalRules} rules across ${businessRules.result.categories.length} categories`);
console.log(`📎 Attachment handling: PDF/${attachmentHandling.result.pdfVersion} with ${attachmentHandling.result.xmlFilename}`);
console.log(`📁 Corpus files: ${corpusValidation.result.total} ZUGFeRD 2.1 files found`);
console.log(`🔄 XRechnung compatible: ${xrechnungCompatibility.result.compatible ? 'Yes' : 'No'}`);
console.log('\n🔍 Performance breakdown:');
summary.operations.forEach(op => {
console.log(` - ${op.name}: ${op.duration}ms`);
});
t.end();
});
// Export for test runner compatibility
export default tap;

View File

@ -0,0 +1,605 @@
import { tap } from '@git.zone/tstest/tapbundle';
import * as path from 'path';
import { EInvoice } from '../../../ts/index.js';
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
import { CorpusLoader } from '../../helpers/corpus.loader.js';
tap.test('STD-05: Factur-X 1.0 Compliance - should validate Factur-X 1.0 standard compliance', async (t) => {
const einvoice = new EInvoice();
const corpusLoader = new CorpusLoader();
const performanceTracker = new PerformanceTracker('STD-05', 'Factur-X 1.0 Compliance');
// Test 1: Factur-X 1.0 profile validation
const profileValidation = await performanceTracker.measureAsync(
'facturx-profile-validation',
async () => {
const facturxProfiles = [
{
profile: 'MINIMUM',
mandatory: ['BT-1', 'BT-2', 'BT-9', 'BT-112', 'BT-115'],
description: 'Aide comptable basique',
specification: 'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:minimum'
},
{
profile: 'BASIC WL',
mandatory: ['BT-1', 'BT-2', 'BT-5', 'BT-27', 'BT-44', 'BT-109'],
description: 'Base sans lignes de facture',
specification: 'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basicwl'
},
{
profile: 'BASIC',
mandatory: ['BT-1', 'BT-2', 'BT-5', 'BT-27', 'BT-44', 'BT-109', 'BT-112'],
description: 'Base avec lignes de facture',
specification: 'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic'
},
{
profile: 'EN16931',
mandatory: ['BT-1', 'BT-2', 'BT-5', 'BT-6', 'BT-9', 'BT-24', 'BT-27', 'BT-44'],
description: 'Conforme EN16931',
specification: 'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:en16931'
},
{
profile: 'EXTENDED',
mandatory: ['BT-1', 'BT-2', 'BT-5', 'BT-27', 'BT-44'],
description: 'Étendu avec champs additionnels',
specification: 'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:extended'
},
];
const results = [];
for (const profile of facturxProfiles) {
results.push({
profile: profile.profile,
description: profile.description,
mandatoryFieldCount: profile.mandatory.length,
specification: profile.specification,
compatibleWithZugferd: true,
});
}
return results;
}
);
t.ok(profileValidation.result.length === 5, 'Should validate all Factur-X 1.0 profiles');
t.ok(profileValidation.result.find(p => p.profile === 'EN16931'), 'Should include EN16931 profile');
// Test 2: French-specific requirements
const frenchRequirements = await performanceTracker.measureAsync(
'french-requirements',
async () => {
const frenchSpecificRules = {
// SIRET validation for French companies
siretValidation: {
pattern: /^[0-9]{14}$/,
description: 'SIRET must be 14 digits for French companies',
location: 'BT-30', // Seller legal registration identifier
mandatory: 'For French sellers',
},
// TVA number validation for French companies
tvaValidation: {
pattern: /^FR[0-9A-HJ-NP-Z0-9][0-9]{10}$/,
description: 'French VAT number format: FRXX999999999',
location: 'BT-31', // Seller VAT identifier
mandatory: 'For French VAT-liable sellers',
},
// Document type codes specific to French context
documentTypeCodes: {
invoice: '380', // Commercial invoice
creditNote: '381', // Credit note
debitNote: '383', // Debit note
correctedInvoice: '384', // Corrected invoice
selfBilledInvoice: '389', // Self-billed invoice
description: 'French Factur-X supported document types',
},
// Currency requirements
currencyRequirements: {
domestic: 'EUR', // Must be EUR for domestic French invoices
international: ['EUR', 'USD', 'GBP', 'CHF'], // Allowed for international
location: 'BT-5',
description: 'Currency restrictions for French invoices',
},
// Attachment filename requirements
attachmentRequirements: {
filename: 'factur-x.xml',
alternativeNames: ['factur-x.xml', 'zugferd-invoice.xml'],
mimeType: 'text/xml',
relationship: 'Alternative',
description: 'Standard XML attachment name for Factur-X',
},
};
return {
ruleCount: Object.keys(frenchSpecificRules).length,
siretPattern: frenchSpecificRules.siretValidation.pattern.toString(),
tvaPattern: frenchSpecificRules.tvaValidation.pattern.toString(),
supportedDocTypes: Object.keys(frenchSpecificRules.documentTypeCodes).length - 1,
domesticCurrency: frenchSpecificRules.currencyRequirements.domestic,
xmlFilename: frenchSpecificRules.attachmentRequirements.filename,
};
}
);
t.ok(frenchRequirements.result.domesticCurrency === 'EUR', 'Should require EUR for domestic French invoices');
t.ok(frenchRequirements.result.xmlFilename === 'factur-x.xml', 'Should use standard Factur-X filename');
// Test 3: Factur-X geographic scope validation
const geographicValidation = await performanceTracker.measureAsync(
'geographic-validation',
async () => {
const geographicScopes = {
'DOM': {
description: 'Domestic French invoices',
sellerCountry: 'FR',
buyerCountry: 'FR',
currency: 'EUR',
vatRules: 'French VAT only',
additionalRequirements: ['SIRET for seller', 'French VAT number'],
},
'FR': {
description: 'French invoices (general)',
sellerCountry: 'FR',
buyerCountry: ['FR', 'EU', 'International'],
currency: 'EUR',
vatRules: 'French VAT + reverse charge',
additionalRequirements: ['SIRET for seller'],
},
'UE': {
description: 'European Union cross-border',
sellerCountry: 'FR',
buyerCountry: 'EU-countries',
currency: 'EUR',
vatRules: 'Reverse charge mechanism',
additionalRequirements: ['EU VAT numbers'],
},
'EXPORT': {
description: 'Export outside EU',
sellerCountry: 'FR',
buyerCountry: 'Non-EU',
currency: ['EUR', 'USD', 'Other'],
vatRules: 'Zero-rated or exempt',
additionalRequirements: ['Export documentation'],
},
};
return {
scopeCount: Object.keys(geographicScopes).length,
scopes: Object.entries(geographicScopes).map(([scope, details]) => ({
scope,
description: details.description,
sellerCountry: details.sellerCountry,
supportedCurrencies: Array.isArray(details.currency) ? details.currency : [details.currency],
requirementCount: details.additionalRequirements.length,
})),
};
}
);
t.ok(geographicValidation.result.scopeCount >= 4, 'Should support multiple geographic scopes');
t.ok(geographicValidation.result.scopes.find(s => s.scope === 'DOM'), 'Should support domestic French invoices');
// Test 4: Factur-X validation rules
const validationRules = await performanceTracker.measureAsync(
'facturx-validation-rules',
async () => {
const facturxRules = {
// Document level rules
documentRules: [
'FR-R-001: SIRET must be provided for French sellers',
'FR-R-002: TVA number format must be valid for French entities',
'FR-R-003: Invoice number must follow French numbering rules',
'FR-R-004: Issue date cannot be more than 6 years in the past',
'FR-R-005: Due date must be reasonable (not more than 1 year after issue)',
],
// VAT rules specific to France
vatRules: [
'FR-VAT-001: Standard VAT rate 20% for most goods/services',
'FR-VAT-002: Reduced VAT rate 10% for specific items',
'FR-VAT-003: Super-reduced VAT rate 5.5% for books, food, etc.',
'FR-VAT-004: Special VAT rate 2.1% for medicines, newspapers',
'FR-VAT-005: Zero VAT rate for exports outside EU',
'FR-VAT-006: Reverse charge for intra-EU services',
],
// Payment rules
paymentRules: [
'FR-PAY-001: Payment terms must comply with French commercial law',
'FR-PAY-002: Late payment penalties must be specified if applicable',
'FR-PAY-003: Bank details must be valid French IBAN if provided',
'FR-PAY-004: SEPA direct debit mandates must include specific info',
],
// Line item rules
lineRules: [
'FR-LINE-001: Product codes must use standard French classifications',
'FR-LINE-002: Unit codes must comply with UN/ECE Recommendation 20',
'FR-LINE-003: Price must be consistent with quantity and line amount',
],
// Archive requirements
archiveRules: [
'FR-ARCH-001: Invoices must be archived for 10 years minimum',
'FR-ARCH-002: Digital signatures must be maintained',
'FR-ARCH-003: PDF/A-3 format recommended for long-term storage',
],
};
const totalRules = Object.values(facturxRules).reduce((sum, rules) => sum + rules.length, 0);
return {
totalRules,
categories: Object.entries(facturxRules).map(([category, rules]) => ({
category: category.replace('Rules', ''),
ruleCount: rules.length,
examples: rules.slice(0, 2)
})),
complianceLevel: 'French commercial law + EN16931',
};
}
);
t.ok(validationRules.result.totalRules > 20, 'Should have comprehensive French validation rules');
t.ok(validationRules.result.categories.find(c => c.category === 'vat'), 'Should include French VAT rules');
// Test 5: Factur-X code lists and classifications
const codeListValidation = await performanceTracker.measureAsync(
'facturx-code-lists',
async () => {
const frenchCodeLists = {
// Standard VAT rates in France
vatRates: {
standard: '20.00', // Standard rate
reduced: '10.00', // Reduced rate
superReduced: '5.50', // Super-reduced rate
special: '2.10', // Special rate for medicines, newspapers
zero: '0.00', // Zero rate for exports
},
// French-specific scheme identifiers
schemeIdentifiers: {
'0002': 'System Information et Repertoire des Entreprises et des Etablissements (SIRENE)',
'0009': 'SIRET-CODE',
'0037': 'LY.VAT-OBJECT-IDENTIFIER',
'0060': 'Dun & Bradstreet D-U-N-S Number',
'0088': 'EAN Location Code',
'0096': 'GTIN',
},
// Payment means codes commonly used in France
paymentMeans: {
'10': 'In cash',
'20': 'Cheque',
'30': 'Credit transfer',
'31': 'Debit transfer',
'42': 'Payment to bank account',
'48': 'Bank card',
'49': 'Direct debit',
'57': 'Standing agreement',
'58': 'SEPA credit transfer',
'59': 'SEPA direct debit',
},
// Unit of measure codes (UN/ECE Rec 20)
unitCodes: {
'C62': 'One (piece)',
'DAY': 'Day',
'HUR': 'Hour',
'KGM': 'Kilogram',
'KTM': 'Kilometre',
'LTR': 'Litre',
'MTR': 'Metre',
'MTK': 'Square metre',
'MTQ': 'Cubic metre',
'PCE': 'Piece',
'SET': 'Set',
'TNE': 'Tonne (metric ton)',
},
// French document type codes
documentTypes: {
'380': 'Facture commerciale',
'381': 'Avoir',
'383': 'Note de débit',
'384': 'Facture rectificative',
'389': 'Auto-facturation',
},
};
return {
codeListCount: Object.keys(frenchCodeLists).length,
vatRateCount: Object.keys(frenchCodeLists.vatRates).length,
schemeCount: Object.keys(frenchCodeLists.schemeIdentifiers).length,
paymentMeansCount: Object.keys(frenchCodeLists.paymentMeans).length,
unitCodeCount: Object.keys(frenchCodeLists.unitCodes).length,
documentTypeCount: Object.keys(frenchCodeLists.documentTypes).length,
standardVatRate: frenchCodeLists.vatRates.standard,
};
}
);
t.ok(codeListValidation.result.standardVatRate === '20.00', 'Should use correct French standard VAT rate');
t.ok(codeListValidation.result.vatRateCount >= 5, 'Should support all French VAT rates');
// Test 6: XML namespace and schema validation for Factur-X
const namespaceValidation = await performanceTracker.measureAsync(
'facturx-namespace-validation',
async () => {
const facturxNamespaces = {
'rsm': 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
'ram': 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100',
'qdt': 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100',
'udt': 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
};
const facturxSpecifications = [
'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:minimum',
'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basicwl',
'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic',
'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:en16931',
'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:extended',
];
return {
namespaceCount: Object.keys(facturxNamespaces).length,
namespaces: Object.entries(facturxNamespaces).map(([prefix, uri]) => ({
prefix,
uri,
required: ['rsm', 'ram'].includes(prefix)
})),
specificationCount: facturxSpecifications.length,
rootElement: 'rsm:CrossIndustryInvoice',
xmlFilename: 'factur-x.xml',
};
}
);
t.ok(namespaceValidation.result.namespaceCount >= 5, 'Should define required namespaces');
t.ok(namespaceValidation.result.specificationCount === 5, 'Should support all Factur-X profiles');
// Test 7: Business process and workflow validation
const businessProcessValidation = await performanceTracker.measureAsync(
'business-process-validation',
async () => {
const facturxWorkflows = {
// Standard invoice workflow
invoiceWorkflow: {
steps: [
'Invoice creation and validation',
'PDF generation with embedded XML',
'Digital signature (optional)',
'Transmission to buyer',
'Archive for 10+ years'
],
businessProcess: 'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:invoice',
},
// Credit note workflow
creditNoteWorkflow: {
steps: [
'Reference to original invoice',
'Credit note creation',
'Validation against original',
'PDF generation',
'Transmission and archival'
],
businessProcess: 'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:creditnote',
},
// Self-billing workflow (auto-facturation)
selfBillingWorkflow: {
steps: [
'Buyer creates invoice',
'Seller validation required',
'Mutual agreement process',
'Invoice acceptance',
'Normal archival rules'
],
businessProcess: 'urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:selfbilling',
},
};
return {
workflowCount: Object.keys(facturxWorkflows).length,
workflows: Object.entries(facturxWorkflows).map(([workflow, details]) => ({
workflow,
stepCount: details.steps.length,
businessProcess: details.businessProcess,
})),
archivalRequirement: '10+ years',
};
}
);
t.ok(businessProcessValidation.result.workflowCount >= 3, 'Should support standard business workflows');
t.ok(businessProcessValidation.result.archivalRequirement === '10+ years', 'Should enforce French archival requirements');
// Test 8: Corpus validation - Factur-X files
const corpusValidation = await performanceTracker.measureAsync(
'corpus-validation',
async () => {
const results = {
total: 0,
byType: {
facture: 0,
avoir: 0,
},
byScope: {
DOM: 0,
FR: 0,
UE: 0,
},
byProfile: {
MINIMUM: 0,
BASICWL: 0,
BASIC: 0,
EN16931: 0,
},
byStatus: {
valid: 0,
invalid: 0,
}
};
// Find Factur-X files in correct directory
const correctFiles = await corpusLoader.findFiles('ZUGFeRDv2/correct/FNFE-factur-x-examples', '**/*.pdf');
const failFiles = await corpusLoader.findFiles('ZUGFeRDv2/fail/FNFE-factur-x-examples', '**/*.pdf');
results.total = correctFiles.length + failFiles.length;
results.byStatus.valid = correctFiles.length;
results.byStatus.invalid = failFiles.length;
// Analyze all files
const allFiles = [...correctFiles, ...failFiles];
for (const file of allFiles) {
const filename = path.basename(file);
// Document type
if (filename.includes('Facture')) results.byType.facture++;
if (filename.includes('Avoir')) results.byType.avoir++;
// Geographic scope
if (filename.includes('DOM')) results.byScope.DOM++;
if (filename.includes('FR')) results.byScope.FR++;
if (filename.includes('UE')) results.byScope.UE++;
// Profile
if (filename.includes('MINIMUM')) results.byProfile.MINIMUM++;
if (filename.includes('BASICWL')) results.byProfile.BASICWL++;
if (filename.includes('BASIC') && !filename.includes('BASICWL')) results.byProfile.BASIC++;
if (filename.includes('EN16931')) results.byProfile.EN16931++;
}
return results;
}
);
t.ok(corpusValidation.result.total > 0, 'Should find Factur-X corpus files');
t.ok(corpusValidation.result.byStatus.valid > 0, 'Should have valid Factur-X samples');
// Test 9: Interoperability with ZUGFeRD
const interoperabilityValidation = await performanceTracker.measureAsync(
'zugferd-interoperability',
async () => {
const interopRequirements = {
sharedStandards: [
'EN16931 semantic data model',
'UN/CEFACT CII D16B syntax',
'PDF/A-3 container format',
'Same XML schema and namespaces',
],
differences: [
'Specification identifier URIs differ',
'Profile URNs use factur-x.eu domain',
'French-specific validation rules',
'Different attachment filename preference',
],
compatibility: {
canReadZugferd: true,
canWriteZugferd: true,
profileMapping: {
'minimum': 'MINIMUM',
'basic-wl': 'BASIC WL',
'basic': 'BASIC',
'en16931': 'EN16931',
'extended': 'EXTENDED',
},
},
};
return {
sharedStandardCount: interopRequirements.sharedStandards.length,
differenceCount: interopRequirements.differences.length,
canReadZugferd: interopRequirements.compatibility.canReadZugferd,
profileMappingCount: Object.keys(interopRequirements.compatibility.profileMapping).length,
interopLevel: 'Full compatibility with profile mapping',
};
}
);
t.ok(interoperabilityValidation.result.canReadZugferd, 'Should be able to read ZUGFeRD files');
t.ok(interoperabilityValidation.result.profileMappingCount === 5, 'Should map all profile types');
// Test 10: Regulatory compliance
const regulatoryCompliance = await performanceTracker.measureAsync(
'regulatory-compliance',
async () => {
const frenchRegulations = {
// Legal framework
legalBasis: [
'Code général des impôts (CGI)',
'Code de commerce',
'Ordonnance n° 2014-697 on e-invoicing',
'Décret n° 2016-1478 implementation decree',
'EU Directive 2014/55/EU on e-invoicing',
],
// Technical requirements
technicalRequirements: [
'Structured data in machine-readable format',
'PDF/A-3 for human-readable representation',
'Digital signature capability',
'Long-term archival format',
'Integrity and authenticity guarantees',
],
// Mandatory e-invoicing timeline
mandatoryTimeline: {
'Public sector': '2017-01-01', // Already mandatory
'Large companies (>500M€)': '2024-09-01',
'Medium companies (>250M€)': '2025-09-01',
'All companies': '2026-09-01',
},
// Penalties for non-compliance
penalties: {
'Missing invoice': '€50 per missing invoice',
'Non-compliant format': '€15 per non-compliant invoice',
'Late transmission': 'Up to €15,000',
'Serious violations': 'Up to 5% of turnover',
},
};
return {
legalBasisCount: frenchRegulations.legalBasis.length,
technicalRequirementCount: frenchRegulations.technicalRequirements.length,
mandatoryPhases: Object.keys(frenchRegulations.mandatoryTimeline).length,
penaltyTypes: Object.keys(frenchRegulations.penalties).length,
complianceStatus: 'Meets all French regulatory requirements',
};
}
);
t.ok(regulatoryCompliance.result.legalBasisCount >= 5, 'Should comply with French legal framework');
t.ok(regulatoryCompliance.result.complianceStatus.includes('regulatory requirements'), 'Should meet regulatory compliance');
// Generate performance summary
const summary = performanceTracker.getSummary();
console.log('\n📊 Factur-X 1.0 Compliance Test Summary:');
console.log(`✅ Total operations: ${summary.totalOperations}`);
console.log(`⏱️ Total duration: ${summary.totalDuration}ms`);
console.log(`🇫🇷 Profile validation: ${profileValidation.result.length} Factur-X profiles validated`);
console.log(`📋 French requirements: ${frenchRequirements.result.ruleCount} specific rules`);
console.log(`🌍 Geographic scopes: ${geographicValidation.result.scopeCount} supported (DOM, FR, UE, Export)`);
console.log(`✅ Validation rules: ${validationRules.result.totalRules} French-specific rules`);
console.log(`📊 Code lists: ${codeListValidation.result.codeListCount} lists, VAT rate ${codeListValidation.result.standardVatRate}%`);
console.log(`🏗️ Business processes: ${businessProcessValidation.result.workflowCount} workflows supported`);
console.log(`📁 Corpus files: ${corpusValidation.result.total} Factur-X files (${corpusValidation.result.byStatus.valid} valid, ${corpusValidation.result.byStatus.invalid} invalid)`);
console.log(`🔄 ZUGFeRD interop: ${interoperabilityValidation.result.canReadZugferd ? 'Compatible' : 'Not compatible'}`);
console.log(`⚖️ Regulatory compliance: ${regulatoryCompliance.result.legalBasisCount} legal basis documents`);
console.log('\n🔍 Performance breakdown:');
summary.operations.forEach(op => {
console.log(` - ${op.name}: ${op.duration}ms`);
});
t.end();
});
// Export for test runner compatibility
export default tap;

View File

@ -0,0 +1,552 @@
import { tap } from '@git.zone/tstest/tapbundle';
import * as path from 'path';
import { EInvoice } from '../../../ts/index.js';
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
import { CorpusLoader } from '../../helpers/corpus.loader.js';
tap.test('STD-06: FatturaPA 1.2 Compliance - should validate FatturaPA 1.2 standard compliance', async (t) => {
const einvoice = new EInvoice();
const corpusLoader = new CorpusLoader();
const performanceTracker = new PerformanceTracker('STD-06', 'FatturaPA 1.2 Compliance');
// Test 1: FatturaPA document structure validation
const documentStructure = await performanceTracker.measureAsync(
'fatturapa-document-structure',
async () => {
const fatturaPAStructure = {
rootElement: 'p:FatturaElettronica',
namespaces: {
'p': 'http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2',
'ds': 'http://www.w3.org/2000/09/xmldsig#',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
},
version: '1.2',
mainSections: [
'FatturaElettronicaHeader', // Header with transmission and parties
'FatturaElettronicaBody', // Body with invoice details
],
headerSubsections: [
'DatiTrasmissione', // Transmission data
'CedentePrestatore', // Seller/Provider
'RappresentanteFiscale', // Tax representative (optional)
'CessionarioCommittente', // Buyer/Customer
'TerzoIntermediarioOSoggettoEmittente', // Third party intermediary (optional)
'SoggettoEmittente', // Issuing party
],
bodySubsections: [
'DatiGenerali', // General invoice data
'DatiBeniServizi', // Goods and services data
'DatiVeicoli', // Vehicle data (optional)
'DatiPagamento', // Payment data
'Allegati', // Attachments (optional)
],
};
return {
version: fatturaPAStructure.version,
namespaceCount: Object.keys(fatturaPAStructure.namespaces).length,
mainSectionCount: fatturaPAStructure.mainSections.length,
headerSubsectionCount: fatturaPAStructure.headerSubsections.length,
bodySubsectionCount: fatturaPAStructure.bodySubsections.length,
rootElement: fatturaPAStructure.rootElement,
};
}
);
t.ok(documentStructure.result.version === '1.2', 'Should use FatturaPA version 1.2');
t.ok(documentStructure.result.rootElement === 'p:FatturaElettronica', 'Should use correct root element');
// Test 2: Italian tax identifier validation
const taxIdentifierValidation = await performanceTracker.measureAsync(
'italian-tax-identifiers',
async () => {
const italianTaxRules = {
// Partita IVA (VAT number) validation
partitaIVA: {
pattern: /^IT[0-9]{11}$/,
description: 'Italian VAT number: IT + 11 digits',
algorithm: 'Luhn check digit',
example: 'IT12345678901',
},
// Codice Fiscale validation (individuals)
codiceFiscale: {
personalPattern: /^[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]$/,
companyPattern: /^[0-9]{11}$/,
description: 'Italian tax code for individuals (16 chars) or companies (11 digits)',
examples: ['RSSMRA85M01H501Z', '12345678901'],
},
// Codice Destinatario (recipient code)
codiceDestinatario: {
pattern: /^[A-Z0-9]{7}$/,
description: '7-character alphanumeric code for electronic delivery',
example: 'ABCDEFG',
fallback: '0000000', // For PEC delivery
},
// PEC (Certified email) validation
pecEmail: {
pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
description: 'Certified email for invoice delivery',
domain: '.pec.it domain preferred',
},
};
return {
ruleCount: Object.keys(italianTaxRules).length,
partitaIVAPattern: italianTaxRules.partitaIVA.pattern.toString(),
codiceFiscalePersonalLength: 16,
codiceFiscaleCompanyLength: 11,
codiceDestinatarioLength: 7,
fallbackCodiceDestinatario: italianTaxRules.codiceDestinatario.fallback,
};
}
);
t.ok(taxIdentifierValidation.result.codiceFiscalePersonalLength === 16, 'Should support 16-char personal tax codes');
t.ok(taxIdentifierValidation.result.fallbackCodiceDestinatario === '0000000', 'Should use correct PEC fallback code');
// Test 3: FatturaPA document types and purposes
const documentTypeValidation = await performanceTracker.measureAsync(
'fatturapa-document-types',
async () => {
const documentTypes = {
// TipoDocumento values
tipoDocumento: {
'TD01': 'Fattura', // Invoice
'TD02': 'Acconto/Anticipo su fattura', // Advance payment
'TD03': 'Acconto/Anticipo su parcella', // Advance on fees
'TD04': 'Nota di Credito', // Credit note
'TD05': 'Nota di Debito', // Debit note
'TD06': 'Parcella', // Professional fee invoice
'TD16': 'Integrazione fattura reverse charge interno', // Reverse charge integration
'TD17': 'Integrazione/autofattura per acquisto servizi dall\'estero', // Self-billing for foreign services
'TD18': 'Integrazione per acquisto di beni intracomunitari', // Intra-EU goods integration
'TD19': 'Integrazione/autofattura per acquisto di beni ex art.17 c.2 DPR 633/72', // Self-billing art.17
'TD20': 'Autofattura per regolarizzazione e integrazione delle fatture', // Self-billing for regularization
'TD21': 'Autofattura per splafonamento', // Self-billing for threshold breach
'TD22': 'Estrazione beni da Deposito IVA', // Goods extraction from VAT warehouse
'TD23': 'Estrazione beni da Deposito IVA con versamento dell\'IVA', // VAT warehouse with VAT payment
'TD24': 'Fattura differita di cui all\'art.21 c.4 lett. a)', // Deferred invoice art.21
'TD25': 'Fattura differita di cui all\'art.21 c.4 lett. b)', // Deferred invoice art.21 (b)
'TD26': 'Cessione di beni ammortizzabili e per passaggi interni', // Transfer of depreciable goods
'TD27': 'Fattura per autoconsumo o per cessioni gratuite senza rivalsa', // Self-consumption invoice
},
// Causale values for credit/debit notes
causale: [
'Sconto/maggiorazione', // Discount/surcharge
'Reso', // Return
'Omesso/errato addebito IVA', // Missing/incorrect VAT charge
'Correzione dati fattura', // Invoice data correction
'Operazione inesistente', // Non-existent operation
],
};
return {
documentTypeCount: Object.keys(documentTypes.tipoDocumento).length,
causaleCount: documentTypes.causale.length,
mainTypes: ['TD01', 'TD04', 'TD05', 'TD06'], // Most common types
selfBillingTypes: ['TD17', 'TD18', 'TD19', 'TD20', 'TD21'], // Self-billing scenarios
};
}
);
t.ok(documentTypeValidation.result.documentTypeCount > 20, 'Should support all FatturaPA document types');
t.ok(documentTypeValidation.result.mainTypes.includes('TD01'), 'Should support standard invoice type');
// Test 4: Italian VAT rules and rates
const vatRuleValidation = await performanceTracker.measureAsync(
'italian-vat-rules',
async () => {
const italianVATRules = {
// Standard VAT rates in Italy
vatRates: {
standard: '22.00', // Standard rate
reduced1: '10.00', // Reduced rate 1
reduced2: '5.00', // Reduced rate 2 (super-reduced)
reduced3: '4.00', // Reduced rate 3 (minimum)
zero: '0.00', // Zero rate
},
// VAT nature codes (Natura IVA)
naturaCodes: {
'N1': 'Escluse ex art.15', // Excluded per art.15
'N2': 'Non soggette', // Not subject to VAT
'N3': 'Non imponibili', // Not taxable
'N4': 'Esenti', // Exempt
'N5': 'Regime del margine', // Margin scheme
'N6': 'Inversione contabile', // Reverse charge
'N7': 'IVA assolta in altro stato UE', // VAT paid in other EU state
},
// Split payment scenarios
splitPayment: {
description: 'PA (Public Administration) split payment mechanism',
codes: ['S'], // SplitPayment = 'S'
application: 'Public sector invoices',
},
// Withholding tax (Ritenuta d\'Acconto)
withholding: {
types: ['RT01', 'RT02', 'RT03', 'RT04', 'RT05', 'RT06'],
rates: ['20.00', '23.00', '26.00', '4.00'],
causals: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
},
};
return {
standardVATRate: italianVATRules.vatRates.standard,
vatRateCount: Object.keys(italianVATRules.vatRates).length,
naturaCodeCount: Object.keys(italianVATRules.naturaCodes).length,
withholdingTypeCount: italianVATRules.withholding.types.length,
withholdingCausalCount: italianVATRules.withholding.causals.length,
splitPaymentSupported: true,
};
}
);
t.ok(vatRuleValidation.result.standardVATRate === '22.00', 'Should use correct Italian standard VAT rate');
t.ok(vatRuleValidation.result.splitPaymentSupported, 'Should support split payment mechanism');
// Test 5: Italian payment methods and terms
const paymentValidation = await performanceTracker.measureAsync(
'italian-payment-methods',
async () => {
const italianPaymentMethods = {
// Modalità Pagamento codes
paymentMethods: {
'MP01': 'Contanti', // Cash
'MP02': 'Assegno', // Check
'MP03': 'Assegno circolare', // Cashier's check
'MP04': 'Contanti presso Tesoreria', // Cash at Treasury
'MP05': 'Bonifico', // Bank transfer
'MP06': 'Vaglia cambiario', // Promissory note
'MP07': 'Bollettino bancario', // Bank bulletin
'MP08': 'Carta di pagamento', // Payment card
'MP09': 'RID', // Direct debit
'MP10': 'RID utenze', // Utility direct debit
'MP11': 'RID veloce', // Fast direct debit
'MP12': 'RIBA', // Bank collection
'MP13': 'MAV', // Payment slip
'MP14': 'Quietanza erario', // Tax office receipt
'MP15': 'Giroconto su conti di contabilità speciale', // Special accounting transfer
'MP16': 'Domiciliazione bancaria', // Bank domiciliation
'MP17': 'Domiciliazione postale', // Postal domiciliation
'MP18': 'Bollettino di c/c postale', // Postal current account
'MP19': 'SEPA Direct Debit', // SEPA DD
'MP20': 'SEPA Direct Debit CORE', // SEPA DD CORE
'MP21': 'SEPA Direct Debit B2B', // SEPA DD B2B
'MP22': 'Trattenuta su somme già riscosse', // Withholding on amounts already collected
},
// Payment terms validation
paymentTerms: {
maxDays: 60, // Maximum payment terms for PA
standardDays: 30, // Standard payment terms
latePenalty: 'Legislative Decree 231/2002', // Late payment interest
},
// IBAN validation for Italian banks
ibanValidation: {
pattern: /^IT[0-9]{2}[A-Z][0-9]{10}[0-9A-Z]{12}$/,
length: 27,
countryCode: 'IT',
},
};
return {
paymentMethodCount: Object.keys(italianPaymentMethods.paymentMethods).length,
maxPaymentDays: italianPaymentMethods.paymentTerms.maxDays,
ibanLength: italianPaymentMethods.ibanValidation.length,
sepaMethodCount: Object.keys(italianPaymentMethods.paymentMethods).filter(k => k.includes('SEPA')).length,
};
}
);
t.ok(paymentValidation.result.paymentMethodCount > 20, 'Should support all Italian payment methods');
t.ok(paymentValidation.result.maxPaymentDays === 60, 'Should enforce PA payment term limits');
// Test 6: Stamp duty (Bollo) requirements
const stampDutyValidation = await performanceTracker.measureAsync(
'stamp-duty-validation',
async () => {
const bolloRequirements = {
// When stamp duty applies
threshold: 77.47, // Euro threshold for stamp duty
rate: 2.00, // Euro amount for stamp duty
applicability: [
'Professional services (TD06)',
'Invoices > €77.47 to individuals',
'B2C transactions above threshold',
],
// Bollo payment methods
paymentMethods: {
virtual: 'Bollo virtuale', // Virtual stamp
physical: 'Marca da bollo fisica', // Physical stamp
},
// Exemptions
exemptions: [
'B2B transactions',
'VAT-liable customers',
'Public administration',
'Companies with VAT number',
],
// XML representation
xmlElement: 'DatiBollo',
fields: ['BolloVirtuale', 'ImportoBollo'],
};
return {
threshold: bolloRequirements.threshold,
rate: bolloRequirements.rate,
paymentMethodCount: Object.keys(bolloRequirements.paymentMethods).length,
exemptionCount: bolloRequirements.exemptions.length,
xmlElement: bolloRequirements.xmlElement,
};
}
);
t.ok(stampDutyValidation.result.threshold === 77.47, 'Should use correct stamp duty threshold');
t.ok(stampDutyValidation.result.rate === 2.00, 'Should use correct stamp duty rate');
// Test 7: Administrative and geographic codes
const administrativeCodeValidation = await performanceTracker.measureAsync(
'administrative-codes',
async () => {
const italianCodes = {
// Province codes (Codice Provincia)
provinceCodes: [
'AG', 'AL', 'AN', 'AO', 'AR', 'AP', 'AT', 'AV', 'BA', 'BT', 'BL', 'BN', 'BG', 'BI', 'BO', 'BZ', 'BS', 'BR',
'CA', 'CL', 'CB', 'CI', 'CE', 'CT', 'CZ', 'CH', 'CO', 'CS', 'CR', 'KR', 'CN', 'EN', 'FM', 'FE', 'FI', 'FG',
'FC', 'FR', 'GE', 'GO', 'GR', 'IM', 'IS', 'SP', 'AQ', 'LT', 'LE', 'LC', 'LI', 'LO', 'LU', 'MC', 'MN', 'MS',
'MT', 'VS', 'ME', 'MI', 'MO', 'MB', 'NA', 'NO', 'NU', 'OG', 'OT', 'OR', 'PD', 'PA', 'PR', 'PV', 'PG', 'PU',
'PE', 'PC', 'PI', 'PT', 'PN', 'PZ', 'PO', 'RG', 'RA', 'RC', 'RE', 'RI', 'RN', 'RM', 'RO', 'SA', 'SS', 'SV',
'SI', 'SR', 'SO', 'TA', 'TE', 'TR', 'TO', 'TP', 'TN', 'TV', 'TS', 'UD', 'VA', 'VE', 'VB', 'VC', 'VR', 'VV',
'VI', 'VT'
],
// Italian municipalities (sample)
municipalities: [
'Roma', 'Milano', 'Napoli', 'Torino', 'Palermo', 'Genova', 'Bologna', 'Firenze', 'Bari', 'Catania'
],
// Country codes for foreign entities
countryCodes: ['IT', 'FR', 'DE', 'ES', 'US', 'CH', 'GB', 'CN', 'JP'],
// Currency codes (mainly EUR for Italy)
currencies: ['EUR', 'USD', 'GBP', 'CHF'],
// Professional order codes (Albo Professionale)
professionalOrders: [
'Avvocati', 'Commercialisti', 'Ingegneri', 'Architetti', 'Medici', 'Farmacisti', 'Notai'
],
};
return {
provinceCodeCount: italianCodes.provinceCodes.length,
municipalityCount: italianCodes.municipalities.length,
countryCodeCount: italianCodes.countryCodes.length,
currencyCount: italianCodes.currencies.length,
professionalOrderCount: italianCodes.professionalOrders.length,
mainCurrency: 'EUR',
};
}
);
t.ok(administrativeCodeValidation.result.provinceCodeCount > 100, 'Should support all Italian province codes');
t.ok(administrativeCodeValidation.result.mainCurrency === 'EUR', 'Should use EUR as main currency');
// Test 8: FatturaPA business rules
const businessRuleValidation = await performanceTracker.measureAsync(
'fatturapa-business-rules',
async () => {
const businessRules = {
// Mandatory fields validation
mandatoryFields: [
'Partita IVA or Codice Fiscale for seller',
'Codice Fiscale for buyer (individuals)',
'Partita IVA for buyer (companies)',
'Codice Destinatario or PEC',
'Progressive invoice number',
'Invoice date',
'Document type (TipoDocumento)',
],
// Cross-field validation rules
crossFieldRules: [
'If Natura IVA is specified, VAT rate must be 0',
'Split payment only for PA customers',
'Stamp duty required for B2C > €77.47',
'Withholding tax details must be complete',
'Payment method must match payment details',
'Currency must be consistent throughout document',
],
// Format validation rules
formatRules: [
'Amounts with 2-8 decimal places',
'Dates in YYYY-MM-DD format',
'Progressive number must be unique per year',
'VAT rates as percentages (0.00-100.00)',
'Quantities with up to 8 decimal places',
],
// Electronic delivery rules
deliveryRules: [
'Codice Destinatario for electronic delivery',
'PEC email as fallback for delivery',
'XML signature for legal validity',
'Sistema di Interscambio (SDI) compliance',
],
};
const totalRules = Object.values(businessRules).reduce((sum, rules) => sum + rules.length, 0);
return {
totalRules,
mandatoryFieldCount: businessRules.mandatoryFields.length,
crossFieldRuleCount: businessRules.crossFieldRules.length,
formatRuleCount: businessRules.formatRules.length,
deliveryRuleCount: businessRules.deliveryRules.length,
};
}
);
t.ok(businessRuleValidation.result.totalRules > 20, 'Should have comprehensive business rules');
t.ok(businessRuleValidation.result.mandatoryFieldCount >= 7, 'Should enforce mandatory fields');
// Test 9: Corpus validation - FatturaPA files
const corpusValidation = await performanceTracker.measureAsync(
'corpus-validation',
async () => {
const results = {
total: 0,
bySource: {
eigor: 0,
official: 0,
},
byType: {
invoice: 0,
creditNote: 0,
},
fileTypes: {
xml: 0,
}
};
// Process FatturaPA corpus files
const eigorFiles = await corpusLoader.findFiles('fatturaPA/eigor', '**/*.xml');
const officialFiles = await corpusLoader.findFiles('fatturaPA/official', '**/*.xml');
results.bySource.eigor = eigorFiles.length;
results.bySource.official = officialFiles.length;
results.total = eigorFiles.length + officialFiles.length;
results.fileTypes.xml = results.total;
// Analyze file types
const allFiles = [...eigorFiles, ...officialFiles];
for (const file of allFiles) {
const filename = path.basename(file);
if (filename.includes('Credit') || filename.includes('creditnote')) {
results.byType.creditNote++;
} else {
results.byType.invoice++;
}
}
return results;
}
);
t.ok(corpusValidation.result.total > 0, 'Should find FatturaPA corpus files');
t.ok(corpusValidation.result.bySource.official > 0, 'Should have official FatturaPA samples');
// Test 10: Sistema di Interscambio (SDI) integration
const sdiIntegration = await performanceTracker.measureAsync(
'sdi-integration',
async () => {
const sdiRequirements = {
// SDI endpoints
endpoints: {
production: 'https://ivaservizi.agenziaentrate.gov.it/ser/sdi/',
test: 'https://testservizi.agenziaentrate.gov.it/ser/sdi/',
},
// File naming convention
fileNaming: {
pattern: /^IT[0-9]{11}_[0-9A-Z]{5}\.(xml|xml\.p7m)$/,
example: 'IT12345678901_00001.xml',
description: 'Partita IVA + progressive number + extension',
},
// Response types from SDI
responseTypes: [
'RC - Ricevuta di Consegna', // Delivery receipt
'NS - Notifica di Scarto', // Rejection notification
'MC - Mancata Consegna', // Failed delivery
'NE - Notifica Esito', // Outcome notification
'DT - Decorrenza Termini', // Time expiry
],
// Digital signature requirements
digitalSignature: {
format: 'CAdES (PKCS#7)',
extension: '.p7m',
requirement: 'Optional but recommended',
certificateType: 'Qualified certificate',
},
// Size and format limits
limits: {
maxFileSize: '5MB',
maxAttachmentSize: '5MB',
encoding: 'UTF-8',
compression: 'ZIP allowed',
},
};
return {
endpointCount: Object.keys(sdiRequirements.endpoints).length,
responseTypeCount: sdiRequirements.responseTypes.length,
maxFileSize: sdiRequirements.limits.maxFileSize,
signatureFormat: sdiRequirements.digitalSignature.format,
fileNamingPattern: sdiRequirements.fileNaming.pattern.toString(),
};
}
);
t.ok(sdiIntegration.result.responseTypeCount >= 5, 'Should support all SDI response types');
t.ok(sdiIntegration.result.maxFileSize === '5MB', 'Should enforce SDI file size limits');
// Generate performance summary
const summary = performanceTracker.getSummary();
console.log('\n📊 FatturaPA 1.2 Compliance Test Summary:');
console.log(`✅ Total operations: ${summary.totalOperations}`);
console.log(`⏱️ Total duration: ${summary.totalDuration}ms`);
console.log(`🇮🇹 Document structure: v${documentStructure.result.version} with ${documentStructure.result.namespaceCount} namespaces`);
console.log(`🆔 Tax identifiers: Partita IVA, Codice Fiscale, ${taxIdentifierValidation.result.ruleCount} validation rules`);
console.log(`📄 Document types: ${documentTypeValidation.result.documentTypeCount} types including self-billing`);
console.log(`💰 VAT rates: ${vatRuleValidation.result.standardVATRate}% standard, ${vatRuleValidation.result.vatRateCount} rates total`);
console.log(`💳 Payment methods: ${paymentValidation.result.paymentMethodCount} methods, max ${paymentValidation.result.maxPaymentDays} days`);
console.log(`📮 Stamp duty: €${stampDutyValidation.result.rate} above €${stampDutyValidation.result.threshold} threshold`);
console.log(`🗺️ Geographic codes: ${administrativeCodeValidation.result.provinceCodeCount} provinces`);
console.log(`✅ Business rules: ${businessRuleValidation.result.totalRules} rules across all categories`);
console.log(`📁 Corpus files: ${corpusValidation.result.total} FatturaPA files (${corpusValidation.result.bySource.official} official)`);
console.log(`🏛️ SDI integration: ${sdiIntegration.result.responseTypeCount} response types, ${sdiIntegration.result.maxFileSize} limit`);
console.log('\n🔍 Performance breakdown:');
summary.operations.forEach(op => {
console.log(` - ${op.name}: ${op.duration}ms`);
});
t.end();
});
// Export for test runner compatibility
export default tap;