- 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.
328 lines
9.6 KiB
TypeScript
328 lines
9.6 KiB
TypeScript
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(); |