feat: Implement PEPPOL and XRechnung validators for compliance with e-invoice specifications
- 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.
This commit is contained in:
328
test/test.peppol-validator.ts
Normal file
328
test/test.peppol-validator.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { PeppolValidator } from '../ts/formats/validation/peppol.validator.js';
|
||||
import type { EInvoice } from '../ts/einvoice.js';
|
||||
|
||||
tap.test('PEPPOL Validator - basic instantiation', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
expect(validator).toBeInstanceOf(PeppolValidator);
|
||||
|
||||
// Singleton pattern
|
||||
const validator2 = PeppolValidator.create();
|
||||
expect(validator2).toEqual(validator);
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - endpoint ID validation', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
extensions: {
|
||||
sellerEndpointId: '0088:1234567890128', // Valid GLN
|
||||
buyerEndpointId: '0192:123456789' // Valid Norwegian org
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
const endpointErrors = results.filter(r => r.ruleId.startsWith('PEPPOL-T00'));
|
||||
|
||||
console.log('Endpoint validation results:', endpointErrors);
|
||||
expect(endpointErrors.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - invalid GLN endpoint', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
extensions: {
|
||||
sellerEndpointId: '0088:123456789012', // Invalid GLN (wrong check digit)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
const endpointErrors = results.filter(r => r.ruleId === 'PEPPOL-T001');
|
||||
|
||||
expect(endpointErrors.length).toBeGreaterThan(0);
|
||||
expect(endpointErrors[0].message).toInclude('Invalid seller endpoint ID');
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - invalid endpoint format', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
extensions: {
|
||||
sellerEndpointId: 'invalid-format', // No scheme
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
const endpointErrors = results.filter(r => r.ruleId === 'PEPPOL-T001');
|
||||
|
||||
expect(endpointErrors.length).toBeGreaterThan(0);
|
||||
expect(endpointErrors[0].severity).toEqual('error');
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - document type validation', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
extensions: {
|
||||
documentTypeId: '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'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
const docTypeErrors = results.filter(r => r.ruleId === 'PEPPOL-T003');
|
||||
|
||||
expect(docTypeErrors.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - process ID validation', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
extensions: {
|
||||
processId: 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
const processErrors = results.filter(r => r.ruleId === 'PEPPOL-T004');
|
||||
|
||||
expect(processErrors.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - invalid process ID', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
extensions: {
|
||||
processId: 'invalid:process:id'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
const processErrors = results.filter(r => r.ruleId === 'PEPPOL-T004');
|
||||
|
||||
expect(processErrors.length).toBeGreaterThan(0);
|
||||
expect(processErrors[0].severity).toEqual('warning');
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - business rules', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
// Missing both buyer reference and purchase order reference
|
||||
},
|
||||
from: {
|
||||
type: 'company',
|
||||
name: 'Test Company'
|
||||
// Missing email
|
||||
}
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
|
||||
// Should have error for missing buyer reference
|
||||
const buyerRefErrors = results.filter(r => r.ruleId === 'PEPPOL-B-01');
|
||||
expect(buyerRefErrors.length).toBeGreaterThan(0);
|
||||
|
||||
// Should have warning for missing seller email
|
||||
const emailWarnings = results.filter(r => r.ruleId === 'PEPPOL-B-02');
|
||||
expect(emailWarnings.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - buyer reference present', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
buyerReference: 'REF-12345'
|
||||
}
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
const buyerRefErrors = results.filter(r => r.ruleId === 'PEPPOL-B-01');
|
||||
|
||||
expect(buyerRefErrors.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - purchase order reference present', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
extensions: {
|
||||
purchaseOrderReference: 'PO-2025-001'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
const buyerRefErrors = results.filter(r => r.ruleId === 'PEPPOL-B-01');
|
||||
|
||||
expect(buyerRefErrors.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - payment means validation', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
extensions: {
|
||||
paymentMeans: {
|
||||
paymentMeansCode: '30' // Valid code for credit transfer
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
const paymentErrors = results.filter(r => r.ruleId === 'PEPPOL-B-04');
|
||||
|
||||
expect(paymentErrors.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - invalid payment means', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
extensions: {
|
||||
paymentMeans: {
|
||||
paymentMeansCode: '999' // Invalid code
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
const paymentErrors = results.filter(r => r.ruleId === 'PEPPOL-B-04');
|
||||
|
||||
expect(paymentErrors.length).toBeGreaterThan(0);
|
||||
expect(paymentErrors[0].severity).toEqual('error');
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - non-PEPPOL invoice skips validation', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:cen.eu:en16931:2017', // Not PEPPOL
|
||||
}
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
|
||||
expect(results.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - scheme ID validation', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
extensions: {
|
||||
buyerPartyId: {
|
||||
schemeId: '0088', // Valid GLN scheme
|
||||
id: '1234567890128'
|
||||
}
|
||||
}
|
||||
},
|
||||
from: {
|
||||
type: 'company',
|
||||
name: 'Test Company',
|
||||
registrationDetails: {
|
||||
partyIdentification: {
|
||||
schemeId: '9906', // Valid IT:VAT scheme
|
||||
id: 'IT12345678901'
|
||||
}
|
||||
}
|
||||
} as any
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
const schemeErrors = results.filter(r =>
|
||||
r.ruleId === 'PEPPOL-T005' || r.ruleId === 'PEPPOL-T006'
|
||||
);
|
||||
|
||||
expect(schemeErrors.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - invalid scheme ID', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
extensions: {
|
||||
buyerPartyId: {
|
||||
schemeId: '9999', // Invalid scheme
|
||||
id: '12345'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
const schemeErrors = results.filter(r => r.ruleId === 'PEPPOL-T006');
|
||||
|
||||
expect(schemeErrors.length).toBeGreaterThan(0);
|
||||
expect(schemeErrors[0].severity).toEqual('warning');
|
||||
});
|
||||
|
||||
tap.test('PEPPOL Validator - B2G detection', async () => {
|
||||
const validator = PeppolValidator.create();
|
||||
|
||||
const invoice: Partial<EInvoice> = {
|
||||
metadata: {
|
||||
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
extensions: {
|
||||
buyerPartyId: {
|
||||
schemeId: '0204', // German government Leitweg-ID
|
||||
id: '991-12345-01'
|
||||
},
|
||||
buyerCategory: 'government'
|
||||
}
|
||||
},
|
||||
to: {
|
||||
type: 'company',
|
||||
name: 'Government Agency'
|
||||
}
|
||||
};
|
||||
|
||||
const results = validator.validatePeppol(invoice as EInvoice);
|
||||
|
||||
// B2G should require endpoint IDs
|
||||
const endpointErrors = results.filter(r =>
|
||||
r.ruleId === 'PEPPOL-T001' || r.ruleId === 'PEPPOL-T002'
|
||||
);
|
||||
|
||||
expect(endpointErrors.length).toBeGreaterThan(0);
|
||||
expect(endpointErrors[0].message).toInclude('mandatory for PEPPOL B2G');
|
||||
});
|
||||
|
||||
export default tap.start();
|
Reference in New Issue
Block a user