- Added PeppolValidator class to validate PEPPOL BIS 3.0 invoices, including checks for endpoint IDs, document type IDs, process IDs, party identification, and business rules. - Implemented validation for GLN check digits, document types, and transport protocols specific to PEPPOL. - Added XRechnungValidator class to validate XRechnung 3.0 invoices, focusing on German-specific requirements such as Leitweg-ID, payment details, seller contact, and tax registration. - Included validation for IBAN and BIC formats, ensuring compliance with SEPA regulations. - Established methods for checking B2G invoice indicators and validating mandatory fields for both validators.
453 lines
13 KiB
TypeScript
453 lines
13 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import { FacturXValidator, FacturXProfile } from '../ts/formats/validation/facturx.validator.js';
|
|
import type { EInvoice } from '../ts/einvoice.js';
|
|
|
|
tap.test('Factur-X Validator - basic instantiation', async () => {
|
|
const validator = FacturXValidator.create();
|
|
expect(validator).toBeInstanceOf(FacturXValidator);
|
|
|
|
// Singleton pattern
|
|
const validator2 = FacturXValidator.create();
|
|
expect(validator2).toEqual(validator);
|
|
});
|
|
|
|
tap.test('Factur-X Validator - profile detection', async () => {
|
|
const validator = FacturXValidator.create();
|
|
|
|
// MINIMUM profile
|
|
const minInvoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:facturx:minimum:2017'
|
|
}
|
|
};
|
|
expect(validator.detectProfile(minInvoice as EInvoice)).toEqual(FacturXProfile.MINIMUM);
|
|
|
|
// BASIC profile
|
|
const basicInvoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:facturx:basic:2017'
|
|
}
|
|
};
|
|
expect(validator.detectProfile(basicInvoice as EInvoice)).toEqual(FacturXProfile.BASIC);
|
|
|
|
// EN16931 profile (Comfort)
|
|
const en16931Invoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:facturx:comfort:2017'
|
|
}
|
|
};
|
|
expect(validator.detectProfile(en16931Invoice as EInvoice)).toEqual(FacturXProfile.EN16931);
|
|
|
|
// EXTENDED profile
|
|
const extendedInvoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:facturx:extended:2017'
|
|
}
|
|
};
|
|
expect(validator.detectProfile(extendedInvoice as EInvoice)).toEqual(FacturXProfile.EXTENDED);
|
|
|
|
// Non-Factur-X invoice
|
|
const otherInvoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:cen.eu:en16931:2017'
|
|
}
|
|
};
|
|
expect(validator.detectProfile(otherInvoice as EInvoice)).toEqual(null);
|
|
});
|
|
|
|
tap.test('Factur-X Validator - MINIMUM profile validation', async () => {
|
|
const validator = FacturXValidator.create();
|
|
|
|
const invoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:facturx:minimum:2017'
|
|
},
|
|
accountingDocId: 'INV-2025-001',
|
|
issueDate: new Date('2025-01-11'),
|
|
accountingDocType: 'invoice',
|
|
currency: 'EUR',
|
|
from: {
|
|
type: 'company',
|
|
name: 'Test Seller',
|
|
vatNumber: 'DE123456789'
|
|
},
|
|
to: {
|
|
type: 'company',
|
|
name: 'Test Buyer'
|
|
},
|
|
totalInvoiceAmount: 119.00,
|
|
totalNetAmount: 100.00,
|
|
totalVatAmount: 19.00
|
|
};
|
|
|
|
const results = validator.validateFacturX(invoice as EInvoice, FacturXProfile.MINIMUM);
|
|
const errors = results.filter(r => r.severity === 'error');
|
|
|
|
console.log('MINIMUM profile validation errors:', errors);
|
|
expect(errors.length).toEqual(0);
|
|
});
|
|
|
|
tap.test('Factur-X Validator - MINIMUM profile missing fields', async () => {
|
|
const validator = FacturXValidator.create();
|
|
|
|
const invoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:facturx:minimum:2017'
|
|
},
|
|
accountingDocId: 'INV-2025-001',
|
|
issueDate: new Date('2025-01-11'),
|
|
// Missing required fields for MINIMUM
|
|
};
|
|
|
|
const results = validator.validateFacturX(invoice as EInvoice, FacturXProfile.MINIMUM);
|
|
const errors = results.filter(r => r.severity === 'error');
|
|
|
|
expect(errors.length).toBeGreaterThan(0);
|
|
expect(errors.some(e => e.field === 'currency')).toBeTrue();
|
|
expect(errors.some(e => e.field === 'from.name')).toBeTrue();
|
|
});
|
|
|
|
tap.test('Factur-X Validator - BASIC profile validation', async () => {
|
|
const validator = FacturXValidator.create();
|
|
|
|
const invoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:facturx:basic:2017'
|
|
},
|
|
accountingDocId: 'INV-2025-001',
|
|
issueDate: new Date('2025-01-11'),
|
|
accountingDocType: 'invoice',
|
|
currency: 'EUR',
|
|
dueDate: new Date('2025-02-11'),
|
|
from: {
|
|
type: 'company',
|
|
name: 'Test Seller',
|
|
vatNumber: 'DE123456789',
|
|
address: 'Test Street 1',
|
|
country: 'DE'
|
|
},
|
|
to: {
|
|
type: 'company',
|
|
name: 'Test Buyer',
|
|
address: 'Buyer Street 1',
|
|
country: 'FR'
|
|
},
|
|
items: [
|
|
{
|
|
position: 1,
|
|
name: 'Test Product',
|
|
unitQuantity: 1,
|
|
unitNetPrice: 100.00,
|
|
unitType: 'C62',
|
|
vatPercentage: 19,
|
|
articleNumber: 'ART-001'
|
|
}
|
|
],
|
|
totalInvoiceAmount: 119.00,
|
|
totalNetAmount: 100.00,
|
|
totalVatAmount: 19.00
|
|
};
|
|
|
|
const results = validator.validateFacturX(invoice as EInvoice, FacturXProfile.BASIC);
|
|
const errors = results.filter(r => r.severity === 'error');
|
|
|
|
console.log('BASIC profile validation errors:', errors);
|
|
expect(errors.length).toEqual(0);
|
|
});
|
|
|
|
tap.test('Factur-X Validator - BASIC profile missing line items', async () => {
|
|
const validator = FacturXValidator.create();
|
|
|
|
const invoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:facturx:basic:2017'
|
|
},
|
|
accountingDocId: 'INV-2025-001',
|
|
issueDate: new Date('2025-01-11'),
|
|
accountingDocType: 'invoice',
|
|
currency: 'EUR',
|
|
dueDate: new Date('2025-02-11'),
|
|
from: {
|
|
type: 'company',
|
|
name: 'Test Seller',
|
|
vatNumber: 'DE123456789',
|
|
address: 'Test Street 1',
|
|
country: 'DE'
|
|
},
|
|
to: {
|
|
type: 'company',
|
|
name: 'Test Buyer',
|
|
address: 'Buyer Street 1',
|
|
country: 'FR'
|
|
},
|
|
// Missing items
|
|
totalInvoiceAmount: 119.00,
|
|
totalNetAmount: 100.00,
|
|
totalVatAmount: 19.00
|
|
};
|
|
|
|
const results = validator.validateFacturX(invoice as EInvoice);
|
|
const errors = results.filter(r => r.severity === 'error');
|
|
|
|
expect(errors.length).toBeGreaterThan(0);
|
|
expect(errors.some(e => e.ruleId === 'FX-BAS-02')).toBeTrue();
|
|
});
|
|
|
|
tap.test('Factur-X Validator - BASIC_WL profile (without lines)', async () => {
|
|
const validator = FacturXValidator.create();
|
|
|
|
const invoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:facturx:basicwl:2017'
|
|
},
|
|
accountingDocId: 'INV-2025-001',
|
|
issueDate: new Date('2025-01-11'),
|
|
accountingDocType: 'invoice',
|
|
currency: 'EUR',
|
|
dueDate: new Date('2025-02-11'),
|
|
from: {
|
|
type: 'company',
|
|
name: 'Test Seller',
|
|
vatNumber: 'DE123456789',
|
|
address: 'Test Street 1',
|
|
country: 'DE'
|
|
},
|
|
to: {
|
|
type: 'company',
|
|
name: 'Test Buyer',
|
|
address: 'Buyer Street 1',
|
|
country: 'FR'
|
|
},
|
|
// No items required for BASIC_WL
|
|
totalInvoiceAmount: 119.00,
|
|
totalNetAmount: 100.00,
|
|
totalVatAmount: 19.00
|
|
};
|
|
|
|
const results = validator.validateFacturX(invoice as EInvoice, FacturXProfile.BASIC_WL);
|
|
const errors = results.filter(r => r.severity === 'error');
|
|
|
|
console.log('BASIC_WL profile validation errors:', errors);
|
|
expect(errors.length).toEqual(0);
|
|
});
|
|
|
|
tap.test('Factur-X Validator - EN16931 profile validation', async () => {
|
|
const validator = FacturXValidator.create();
|
|
|
|
const invoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:facturx:en16931:2017',
|
|
buyerReference: 'REF-12345'
|
|
},
|
|
accountingDocId: 'INV-2025-001',
|
|
issueDate: new Date('2025-01-11'),
|
|
accountingDocType: 'invoice',
|
|
currency: 'EUR',
|
|
dueDate: new Date('2025-02-11'),
|
|
from: {
|
|
type: 'company',
|
|
name: 'Test Seller',
|
|
vatNumber: 'DE123456789',
|
|
address: 'Test Street 1',
|
|
city: 'Berlin',
|
|
postalCode: '10115',
|
|
country: 'DE'
|
|
},
|
|
to: {
|
|
type: 'company',
|
|
name: 'Test Buyer',
|
|
address: 'Buyer Street 1',
|
|
city: 'Paris',
|
|
postalCode: '75001',
|
|
country: 'FR'
|
|
},
|
|
items: [
|
|
{
|
|
position: 1,
|
|
name: 'Test Product',
|
|
unitQuantity: 1,
|
|
unitNetPrice: 100.00,
|
|
unitType: 'C62',
|
|
vatPercentage: 19,
|
|
articleNumber: 'ART-001'
|
|
}
|
|
],
|
|
totalInvoiceAmount: 119.00,
|
|
totalNetAmount: 100.00,
|
|
totalVatAmount: 19.00
|
|
};
|
|
|
|
const results = validator.validateFacturX(invoice as EInvoice, FacturXProfile.EN16931);
|
|
const errors = results.filter(r => r.severity === 'error');
|
|
|
|
console.log('EN16931 profile validation errors:', errors);
|
|
expect(errors.length).toEqual(0);
|
|
});
|
|
|
|
tap.test('Factur-X Validator - EN16931 missing buyer reference', async () => {
|
|
const validator = FacturXValidator.create();
|
|
|
|
const invoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:facturx:en16931:2017',
|
|
// Missing buyerReference or purchaseOrderReference
|
|
},
|
|
accountingDocId: 'INV-2025-001',
|
|
issueDate: new Date('2025-01-11'),
|
|
accountingDocType: 'invoice',
|
|
currency: 'EUR',
|
|
from: {
|
|
type: 'company',
|
|
name: 'Test Seller',
|
|
vatNumber: 'DE123456789',
|
|
address: 'Test Street 1',
|
|
city: 'Berlin',
|
|
postalCode: '10115',
|
|
country: 'DE'
|
|
},
|
|
to: {
|
|
type: 'company',
|
|
name: 'Test Buyer',
|
|
address: 'Buyer Street 1',
|
|
city: 'Paris',
|
|
postalCode: '75001',
|
|
country: 'FR'
|
|
},
|
|
items: [],
|
|
totalInvoiceAmount: 0,
|
|
totalNetAmount: 0,
|
|
totalVatAmount: 0,
|
|
dueDate: new Date('2025-02-11')
|
|
};
|
|
|
|
const results = validator.validateFacturX(invoice as EInvoice);
|
|
const errors = results.filter(r => r.severity === 'error');
|
|
|
|
expect(errors.some(e => e.ruleId === 'FX-EN-01')).toBeTrue();
|
|
});
|
|
|
|
tap.test('Factur-X Validator - EXTENDED profile validation', async () => {
|
|
const validator = FacturXValidator.create();
|
|
|
|
const invoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:facturx:extended:2017',
|
|
extensions: {
|
|
attachments: [
|
|
{
|
|
filename: 'invoice.pdf',
|
|
mimeType: 'application/pdf',
|
|
data: 'base64encodeddata'
|
|
}
|
|
]
|
|
}
|
|
},
|
|
accountingDocId: 'INV-2025-001',
|
|
issueDate: new Date('2025-01-11'),
|
|
accountingDocType: 'invoice',
|
|
currency: 'EUR',
|
|
from: {
|
|
type: 'company',
|
|
name: 'Test Seller',
|
|
vatNumber: 'DE123456789'
|
|
},
|
|
to: {
|
|
type: 'company',
|
|
name: 'Test Buyer'
|
|
},
|
|
totalInvoiceAmount: 119.00
|
|
};
|
|
|
|
const results = validator.validateFacturX(invoice as EInvoice, FacturXProfile.EXTENDED);
|
|
const errors = results.filter(r => r.severity === 'error');
|
|
|
|
console.log('EXTENDED profile validation errors:', errors);
|
|
expect(errors.length).toEqual(0);
|
|
});
|
|
|
|
tap.test('Factur-X Validator - EXTENDED profile attachment validation', async () => {
|
|
const validator = FacturXValidator.create();
|
|
|
|
const invoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:facturx:extended:2017',
|
|
extensions: {
|
|
attachments: [
|
|
{
|
|
// Missing filename and mimeType
|
|
data: 'base64encodeddata'
|
|
}
|
|
]
|
|
}
|
|
},
|
|
accountingDocId: 'INV-2025-001',
|
|
issueDate: new Date('2025-01-11'),
|
|
accountingDocType: 'invoice',
|
|
currency: 'EUR',
|
|
from: {
|
|
type: 'company',
|
|
name: 'Test Seller',
|
|
vatNumber: 'DE123456789'
|
|
},
|
|
to: {
|
|
type: 'company',
|
|
name: 'Test Buyer'
|
|
},
|
|
totalInvoiceAmount: 119.00
|
|
};
|
|
|
|
const results = validator.validateFacturX(invoice as EInvoice);
|
|
const warnings = results.filter(r => r.severity === 'warning');
|
|
|
|
expect(warnings.some(w => w.ruleId === 'FX-EXT-01')).toBeTrue();
|
|
});
|
|
|
|
tap.test('Factur-X Validator - ZUGFeRD compatibility', async () => {
|
|
const validator = FacturXValidator.create();
|
|
|
|
const invoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:zugferd:basic:2017' // ZUGFeRD format
|
|
}
|
|
};
|
|
|
|
// Should detect as Factur-X (ZUGFeRD is the German name)
|
|
const profile = validator.detectProfile(invoice as EInvoice);
|
|
expect(profile).toEqual(FacturXProfile.BASIC);
|
|
});
|
|
|
|
tap.test('Factur-X Validator - profile display names', async () => {
|
|
const validator = FacturXValidator.create();
|
|
|
|
expect(validator.getProfileDisplayName(FacturXProfile.MINIMUM)).toEqual('Factur-X MINIMUM');
|
|
expect(validator.getProfileDisplayName(FacturXProfile.BASIC)).toEqual('Factur-X BASIC');
|
|
expect(validator.getProfileDisplayName(FacturXProfile.BASIC_WL)).toEqual('Factur-X BASIC WL');
|
|
expect(validator.getProfileDisplayName(FacturXProfile.EN16931)).toEqual('Factur-X EN16931');
|
|
expect(validator.getProfileDisplayName(FacturXProfile.EXTENDED)).toEqual('Factur-X EXTENDED');
|
|
});
|
|
|
|
tap.test('Factur-X Validator - profile compliance levels', async () => {
|
|
const validator = FacturXValidator.create();
|
|
|
|
expect(validator.getProfileComplianceLevel(FacturXProfile.MINIMUM)).toEqual(1);
|
|
expect(validator.getProfileComplianceLevel(FacturXProfile.BASIC_WL)).toEqual(2);
|
|
expect(validator.getProfileComplianceLevel(FacturXProfile.BASIC)).toEqual(3);
|
|
expect(validator.getProfileComplianceLevel(FacturXProfile.EN16931)).toEqual(4);
|
|
expect(validator.getProfileComplianceLevel(FacturXProfile.EXTENDED)).toEqual(5);
|
|
});
|
|
|
|
tap.test('Factur-X Validator - non-Factur-X invoice skips validation', async () => {
|
|
const validator = FacturXValidator.create();
|
|
|
|
const invoice: Partial<EInvoice> = {
|
|
metadata: {
|
|
profileId: 'urn:cen.eu:en16931:2017' // Not Factur-X
|
|
}
|
|
};
|
|
|
|
const results = validator.validateFacturX(invoice as EInvoice);
|
|
|
|
expect(results.length).toEqual(0);
|
|
});
|
|
|
|
export default tap.start(); |