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 ` urn:cen.eu:en16931:2017 INV-001 2024-01-15 2024-02-15 380 EUR Seller Company Buyer Company 1000.00 1000.00 1190.00 1190.00 1 10 1000.00 100.00 `; } 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>/, ''); } if (fieldsToOmit.includes('BT-5')) { invoice = invoice.replace(/.*?<\/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('1000.00', '900.00'); case 'BR-CO-15': // Total with VAT != Total without VAT + VAT return base.replace('1190.00', '1200.00'); default: return base; } } function createUBLEN16931Invoice(): string { return createCompleteEN16931Invoice(); } function createCIIEN16931Invoice(): string { return ` urn:cen.eu:en16931:2017 INV-001 380 20240115 `; } 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' ? 'Value' : 'Value'; return base.replace('', `${extension}`); } 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();