einvoice/test/suite/einvoice_standards-compliance/test.std-05.facturx-10.ts
2025-05-26 04:04:51 +00:00

605 lines
23 KiB
TypeScript

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;