/** * PEPPOL BIS 3.0 validator for compliance with PEPPOL e-invoice specifications * Implements PEPPOL-specific validation rules on top of EN16931 */ import type { ValidationResult } from './validation.types.js'; import type { EInvoice } from '../../einvoice.js'; /** * PEPPOL BIS 3.0 Validator * Implements PEPPOL-specific validation rules and constraints */ export class PeppolValidator { private static instance: PeppolValidator; /** * Singleton pattern for validator instance */ public static create(): PeppolValidator { if (!PeppolValidator.instance) { PeppolValidator.instance = new PeppolValidator(); } return PeppolValidator.instance; } /** * Main validation entry point for PEPPOL */ public validatePeppol(invoice: EInvoice): ValidationResult[] { const results: ValidationResult[] = []; // Check if this is a PEPPOL invoice if (!this.isPeppolInvoice(invoice)) { return results; // Not a PEPPOL invoice, skip validation } // Run all PEPPOL validations results.push(...this.validateEndpointId(invoice)); results.push(...this.validateDocumentTypeId(invoice)); results.push(...this.validateProcessId(invoice)); results.push(...this.validatePartyIdentification(invoice)); results.push(...this.validatePeppolBusinessRules(invoice)); results.push(...this.validateSchemeIds(invoice)); results.push(...this.validateTransportProtocol(invoice)); return results; } /** * Check if invoice is PEPPOL */ private isPeppolInvoice(invoice: EInvoice): boolean { const profileId = invoice.metadata?.profileId || ''; const customizationId = invoice.metadata?.customizationId || ''; const peppolProfiles = [ 'urn:fdc:peppol.eu:2017:poacc:billing:3.0', 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0', 'peppol-bis-3', 'peppol' ]; return peppolProfiles.some(profile => profileId.toLowerCase().includes(profile.toLowerCase()) || customizationId.toLowerCase().includes(profile.toLowerCase()) ); } /** * Validate Endpoint ID format (0088:xxxxxxxxx or other schemes) * PEPPOL-T001, PEPPOL-T002 */ private validateEndpointId(invoice: EInvoice): ValidationResult[] { const results: ValidationResult[] = []; // Check seller endpoint ID const sellerEndpointId = invoice.metadata?.extensions?.sellerEndpointId || invoice.metadata?.extensions?.peppolSellerEndpoint; if (sellerEndpointId) { if (!this.isValidEndpointId(sellerEndpointId)) { results.push({ ruleId: 'PEPPOL-T001', severity: 'error', message: 'Invalid seller endpoint ID format. Expected format: scheme:identifier (e.g., 0088:1234567890128)', field: 'metadata.extensions.sellerEndpointId', value: sellerEndpointId, source: 'PEPPOL' }); } } else if (this.isPeppolB2G(invoice)) { // Endpoint ID is mandatory for B2G results.push({ ruleId: 'PEPPOL-T001', severity: 'error', message: 'Seller endpoint ID is mandatory for PEPPOL B2G invoices', field: 'metadata.extensions.sellerEndpointId', source: 'PEPPOL' }); } // Check buyer endpoint ID const buyerEndpointId = invoice.metadata?.extensions?.buyerEndpointId || invoice.metadata?.extensions?.peppolBuyerEndpoint; if (buyerEndpointId) { if (!this.isValidEndpointId(buyerEndpointId)) { results.push({ ruleId: 'PEPPOL-T002', severity: 'error', message: 'Invalid buyer endpoint ID format. Expected format: scheme:identifier (e.g., 0088:1234567890128)', field: 'metadata.extensions.buyerEndpointId', value: buyerEndpointId, source: 'PEPPOL' }); } } else if (this.isPeppolB2G(invoice)) { // Endpoint ID is mandatory for B2G results.push({ ruleId: 'PEPPOL-T002', severity: 'error', message: 'Buyer endpoint ID is mandatory for PEPPOL B2G invoices', field: 'metadata.extensions.buyerEndpointId', source: 'PEPPOL' }); } return results; } /** * Validate endpoint ID format */ private isValidEndpointId(endpointId: string): boolean { // PEPPOL endpoint ID format: scheme:identifier // Common schemes: 0088 (GLN), 0192 (Norwegian org), 9906 (IT VAT), etc. const endpointPattern = /^[0-9]{4}:[A-Za-z0-9\-._]+$/; // Special validation for GLN (0088) if (endpointId.startsWith('0088:')) { const gln = endpointId.substring(5); // GLN should be 13 digits if (!/^\d{13}$/.test(gln)) { return false; } // Validate GLN check digit return this.validateGLNCheckDigit(gln); } return endpointPattern.test(endpointId); } /** * Validate GLN check digit using modulo 10 */ private validateGLNCheckDigit(gln: string): boolean { if (gln.length !== 13) return false; let sum = 0; for (let i = 0; i < 12; i++) { const digit = parseInt(gln[i], 10); sum += digit * (i % 2 === 0 ? 1 : 3); } const checkDigit = (10 - (sum % 10)) % 10; return checkDigit === parseInt(gln[12], 10); } /** * Validate Document Type ID * PEPPOL-T003 */ private validateDocumentTypeId(invoice: EInvoice): ValidationResult[] { const results: ValidationResult[] = []; const documentTypeId = invoice.metadata?.extensions?.documentTypeId || invoice.metadata?.extensions?.peppolDocumentType; if (!documentTypeId && this.isPeppolB2G(invoice)) { results.push({ ruleId: 'PEPPOL-T003', severity: 'error', message: 'Document type ID is mandatory for PEPPOL invoices', field: 'metadata.extensions.documentTypeId', source: 'PEPPOL' }); } else if (documentTypeId) { // Validate against known PEPPOL document types const validDocumentTypes = [ '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', 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2::CreditNote##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1', // Add more valid document types as needed ]; if (!validDocumentTypes.some(type => documentTypeId.includes(type))) { results.push({ ruleId: 'PEPPOL-T003', severity: 'warning', message: 'Document type ID may not be a valid PEPPOL document type', field: 'metadata.extensions.documentTypeId', value: documentTypeId, source: 'PEPPOL' }); } } return results; } /** * Validate Process ID * PEPPOL-T004 */ private validateProcessId(invoice: EInvoice): ValidationResult[] { const results: ValidationResult[] = []; const processId = invoice.metadata?.extensions?.processId || invoice.metadata?.extensions?.peppolProcessId; if (!processId && this.isPeppolB2G(invoice)) { results.push({ ruleId: 'PEPPOL-T004', severity: 'error', message: 'Process ID is mandatory for PEPPOL invoices', field: 'metadata.extensions.processId', source: 'PEPPOL' }); } else if (processId) { // Validate against known PEPPOL processes const validProcessIds = [ 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0', 'urn:fdc:peppol.eu:2017:poacc:billing:3.0', // Legacy process IDs 'urn:www.cenbii.eu:profile:bii05:ver2.0', 'urn:www.cenbii.eu:profile:bii04:ver2.0' ]; if (!validProcessIds.includes(processId)) { results.push({ ruleId: 'PEPPOL-T004', severity: 'warning', message: 'Process ID may not be a valid PEPPOL process', field: 'metadata.extensions.processId', value: processId, source: 'PEPPOL' }); } } return results; } /** * Validate Party Identification Schemes * PEPPOL-T005, PEPPOL-T006 */ private validatePartyIdentification(invoice: EInvoice): ValidationResult[] { const results: ValidationResult[] = []; // Validate seller party identification if (invoice.from?.type === 'company') { const company = invoice.from as any; const partyId = company.registrationDetails?.peppolPartyId || company.registrationDetails?.partyIdentification; if (partyId && partyId.schemeId) { if (!this.isValidSchemeId(partyId.schemeId)) { results.push({ ruleId: 'PEPPOL-T005', severity: 'warning', message: 'Seller party identification scheme may not be valid', field: 'from.registrationDetails.partyIdentification.schemeId', value: partyId.schemeId, source: 'PEPPOL' }); } } } // Validate buyer party identification const buyerPartyId = invoice.metadata?.extensions?.buyerPartyId; if (buyerPartyId && buyerPartyId.schemeId) { if (!this.isValidSchemeId(buyerPartyId.schemeId)) { results.push({ ruleId: 'PEPPOL-T006', severity: 'warning', message: 'Buyer party identification scheme may not be valid', field: 'metadata.extensions.buyerPartyId.schemeId', value: buyerPartyId.schemeId, source: 'PEPPOL' }); } } return results; } /** * Validate scheme IDs against PEPPOL code list */ private isValidSchemeId(schemeId: string): boolean { // PEPPOL Party Identifier Scheme (subset of ISO 6523 ICD list) const validSchemes = [ '0002', // System Information et Repertoire des Entreprise et des Etablissements (SIRENE) '0007', // Organisationsnummer (Swedish legal entities) '0009', // SIRET '0037', // LY-tunnus (Finnish business ID) '0060', // DUNS number '0088', // EAN Location Code (GLN) '0096', // VIOC (Danish CVR) '0097', // Danish Ministry of the Interior and Health '0106', // Netherlands Chamber of Commerce '0130', // Direktoratet for forvaltning og IKT (DIFI) '0135', // IT:SIA '0142', // IT:SECETI '0184', // Danish CVR '0190', // Dutch Originator's Identification Number '0191', // Centre of Registers and Information Systems of the Ministry of Justice (Estonia) '0192', // Norwegian Legal Entity '0193', // UBL.BE party identifier '0195', // Singapore UEN '0196', // Kennitala (Iceland) '0198', // ERSTORG '0199', // Legal Entity Identifier (LEI) '0200', // Legal entity code (Lithuania) '0201', // CODICE UNIVOCO UNITÀ ORGANIZZATIVA '0204', // German Leitweg-ID '0208', // Belgian enterprise number '0209', // GS1 identification keys '0210', // CODICE FISCALE '0211', // PARTITA IVA '0212', // Finnish Organization Number '0213', // Finnish VAT number '9901', // Danish CVR '9902', // Danish SE '9904', // German VAT number '9905', // German Leitweg ID '9906', // IT:VAT '9907', // IT:CF '9910', // HU:VAT '9914', // AT:VAT '9915', // AT:GOV '9917', // Netherlands OIN '9918', // IS:KT '9919', // IS company code '9920', // ES:VAT '9922', // AD:VAT '9923', // AL:VAT '9924', // BA:VAT '9925', // BE:VAT '9926', // BG:VAT '9927', // CH:VAT '9928', // CY:VAT '9929', // CZ:VAT '9930', // DE:VAT '9931', // EE:VAT '9932', // GB:VAT '9933', // GR:VAT '9934', // HR:VAT '9935', // IE:VAT '9936', // LI:VAT '9937', // LT:VAT '9938', // LU:VAT '9939', // LV:VAT '9940', // MC:VAT '9941', // ME:VAT '9942', // MK:VAT '9943', // MT:VAT '9944', // NL:VAT '9945', // PL:VAT '9946', // PT:VAT '9947', // RO:VAT '9948', // RS:VAT '9949', // SI:VAT '9950', // SK:VAT '9951', // SM:VAT '9952', // TR:VAT '9953', // VA:VAT '9955', // SE:VAT '9956', // BE:CBE '9957', // FR:VAT '9958', // German Leitweg ID ]; return validSchemes.includes(schemeId); } /** * Validate PEPPOL-specific business rules */ private validatePeppolBusinessRules(invoice: EInvoice): ValidationResult[] { const results: ValidationResult[] = []; // PEPPOL-B-01: Invoice must have a buyer reference or purchase order reference const purchaseOrderRef = invoice.metadata?.extensions?.purchaseOrderReference; if (!invoice.metadata?.buyerReference && !purchaseOrderRef) { results.push({ ruleId: 'PEPPOL-B-01', severity: 'error', message: 'Invoice must have either a buyer reference (BT-10) or purchase order reference (BT-13)', field: 'metadata.buyerReference', source: 'PEPPOL' }); } // PEPPOL-B-02: Seller electronic address is mandatory const sellerEmail = invoice.from?.type === 'company' ? (invoice.from as any).contact?.email : (invoice.from as any)?.email; if (!sellerEmail) { results.push({ ruleId: 'PEPPOL-B-02', severity: 'warning', message: 'Seller electronic address (email) is recommended for PEPPOL invoices', field: 'from.contact.email', source: 'PEPPOL' }); } // PEPPOL-B-03: Item standard identifier if (invoice.items && invoice.items.length > 0) { invoice.items.forEach((item, index) => { const itemId = (item as any).standardItemIdentification; if (!itemId) { results.push({ ruleId: 'PEPPOL-B-03', severity: 'info', message: `Item ${index + 1} should have a standard item identification (GTIN, EAN, etc.)`, field: `items[${index}].standardItemIdentification`, source: 'PEPPOL' }); } else if (itemId.schemeId === '0160' && !this.isValidGTIN(itemId.id)) { // Validate GTIN if scheme is 0160 results.push({ ruleId: 'PEPPOL-B-03', severity: 'error', message: `Item ${index + 1} has invalid GTIN`, field: `items[${index}].standardItemIdentification.id`, value: itemId.id, source: 'PEPPOL' }); } }); } // PEPPOL-B-04: Payment means code must be from UNCL4461 const paymentMeansCode = invoice.metadata?.extensions?.paymentMeans?.paymentMeansCode; if (paymentMeansCode) { const validPaymentMeans = [ '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '60', '61', '62', '63', '64', '65', '66', '67', '68', '70', '74', '75', '76', '77', '78', '91', '92', '93', '94', '95', '96', '97', 'ZZZ' ]; if (!validPaymentMeans.includes(paymentMeansCode)) { results.push({ ruleId: 'PEPPOL-B-04', severity: 'error', message: 'Payment means code must be from UNCL4461 code list', field: 'metadata.extensions.paymentMeans.paymentMeansCode', value: paymentMeansCode, source: 'PEPPOL' }); } } return results; } /** * Validate GTIN (Global Trade Item Number) */ private isValidGTIN(gtin: string): boolean { // GTIN can be 8, 12, 13, or 14 digits if (!/^(\d{8}|\d{12}|\d{13}|\d{14})$/.test(gtin)) { return false; } // Validate check digit const digits = gtin.split('').map(d => parseInt(d, 10)); const checkDigit = digits[digits.length - 1]; let sum = 0; for (let i = digits.length - 2; i >= 0; i--) { const multiplier = ((digits.length - 2 - i) % 2 === 0) ? 3 : 1; sum += digits[i] * multiplier; } const calculatedCheck = (10 - (sum % 10)) % 10; return calculatedCheck === checkDigit; } /** * Validate scheme IDs used in the invoice */ private validateSchemeIds(invoice: EInvoice): ValidationResult[] { const results: ValidationResult[] = []; // Check tax scheme ID const taxSchemeId = invoice.metadata?.extensions?.taxDetails?.[0]?.taxScheme?.id; if (taxSchemeId && taxSchemeId !== 'VAT') { results.push({ ruleId: 'PEPPOL-S-01', severity: 'warning', message: 'Tax scheme ID should be "VAT" for PEPPOL invoices', field: 'metadata.extensions.taxDetails[0].taxScheme.id', value: taxSchemeId, source: 'PEPPOL' }); } // Check currency code is from ISO 4217 if (invoice.currency) { // This is already validated by CodeListValidator, but we can add PEPPOL-specific check if (!['EUR', 'USD', 'GBP', 'SEK', 'NOK', 'DKK', 'CHF', 'PLN', 'CZK', 'HUF'].includes(invoice.currency)) { results.push({ ruleId: 'PEPPOL-S-02', severity: 'info', message: `Currency ${invoice.currency} is uncommon in PEPPOL network`, field: 'currency', value: invoice.currency, source: 'PEPPOL' }); } } return results; } /** * Validate transport protocol requirements */ private validateTransportProtocol(invoice: EInvoice): ValidationResult[] { const results: ValidationResult[] = []; // Check if transport protocol is specified const transportProtocol = invoice.metadata?.extensions?.transportProtocol; if (transportProtocol) { const validProtocols = ['AS2', 'AS4']; if (!validProtocols.includes(transportProtocol)) { results.push({ ruleId: 'PEPPOL-P-01', severity: 'warning', message: 'Transport protocol should be AS2 or AS4 for PEPPOL', field: 'metadata.extensions.transportProtocol', value: transportProtocol, source: 'PEPPOL' }); } } // Check if SMP lookup is possible const sellerEndpointId = invoice.metadata?.extensions?.sellerEndpointId; if (sellerEndpointId && !invoice.metadata?.extensions?.smpRegistered) { results.push({ ruleId: 'PEPPOL-P-02', severity: 'info', message: 'Seller endpoint should be registered in PEPPOL SMP for discovery', field: 'metadata.extensions.smpRegistered', source: 'PEPPOL' }); } return results; } /** * Check if invoice is B2G (Business to Government) */ private isPeppolB2G(invoice: EInvoice): boolean { // Check if buyer has government indicators const buyerSchemeId = invoice.metadata?.extensions?.buyerPartyId?.schemeId; const buyerCategory = invoice.metadata?.extensions?.buyerCategory; // Government scheme IDs often include specific codes const governmentSchemes = ['0204', '9905', '0197', '0215']; // Check various indicators for government entity return buyerCategory === 'government' || (buyerSchemeId && governmentSchemes.includes(buyerSchemeId)) || invoice.metadata?.extensions?.isB2G === true; } }