- 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.
589 lines
19 KiB
TypeScript
589 lines
19 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|
|
} |