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.
2025-08-11 18:07:01 +00:00
|
|
|
/**
|
|
|
|
* Adapter for converting between EInvoice and EN16931 Semantic Model
|
|
|
|
* Provides bidirectional conversion capabilities
|
|
|
|
*/
|
|
|
|
|
|
|
|
import { EInvoice } from '../../einvoice.js';
|
|
|
|
import type {
|
|
|
|
EN16931SemanticModel,
|
|
|
|
Seller,
|
|
|
|
Buyer,
|
|
|
|
PostalAddress,
|
|
|
|
Contact,
|
|
|
|
InvoiceLine,
|
|
|
|
VATBreakdown,
|
|
|
|
DocumentTotals,
|
|
|
|
PaymentInstructions,
|
|
|
|
Allowance,
|
|
|
|
Charge,
|
|
|
|
Period,
|
|
|
|
DeliveryInformation,
|
|
|
|
PriceDetails,
|
|
|
|
VATInformation,
|
|
|
|
ItemInformation
|
|
|
|
} from './bt-bg.model.js';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adapter for converting between EInvoice and EN16931 Semantic Model
|
|
|
|
*/
|
|
|
|
export class SemanticModelAdapter {
|
|
|
|
/**
|
|
|
|
* Convert EInvoice to EN16931 Semantic Model
|
|
|
|
*/
|
|
|
|
public toSemanticModel(invoice: EInvoice): EN16931SemanticModel {
|
|
|
|
return {
|
|
|
|
// Core document information
|
|
|
|
documentInformation: {
|
|
|
|
invoiceNumber: invoice.accountingDocId,
|
|
|
|
issueDate: invoice.issueDate,
|
|
|
|
typeCode: this.mapInvoiceType(invoice.accountingDocType),
|
|
|
|
currencyCode: invoice.currency,
|
|
|
|
notes: invoice.notes ? this.mapNotes(invoice.notes) : undefined
|
|
|
|
},
|
|
|
|
|
|
|
|
// Process metadata
|
|
|
|
processControl: invoice.metadata?.profileId ? {
|
2025-08-11 18:55:30 +00:00
|
|
|
businessProcessType: invoice.metadata?.extensions?.businessProcessId,
|
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.
2025-08-11 18:07:01 +00:00
|
|
|
specificationIdentifier: invoice.metadata.profileId
|
|
|
|
} : undefined,
|
|
|
|
|
|
|
|
// References
|
|
|
|
references: {
|
|
|
|
buyerReference: invoice.metadata?.buyerReference,
|
2025-08-11 18:55:30 +00:00
|
|
|
projectReference: invoice.metadata?.extensions?.projectReference,
|
|
|
|
contractReference: invoice.metadata?.extensions?.contractReference,
|
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.
2025-08-11 18:07:01 +00:00
|
|
|
purchaseOrderReference: invoice.metadata?.extensions?.purchaseOrderReference,
|
|
|
|
salesOrderReference: invoice.metadata?.extensions?.salesOrderReference,
|
|
|
|
precedingInvoices: invoice.metadata?.extensions?.precedingInvoices
|
|
|
|
},
|
|
|
|
|
|
|
|
// Seller
|
|
|
|
seller: {
|
|
|
|
...this.mapSeller(invoice.from),
|
|
|
|
postalAddress: this.mapAddress(invoice.from),
|
|
|
|
contact: this.mapContact(invoice.from)
|
|
|
|
},
|
|
|
|
|
|
|
|
// Buyer
|
|
|
|
buyer: {
|
|
|
|
...this.mapBuyer(invoice.to),
|
|
|
|
postalAddress: this.mapAddress(invoice.to),
|
|
|
|
contact: this.mapContact(invoice.to)
|
|
|
|
},
|
|
|
|
|
|
|
|
// Payee (if different from seller)
|
|
|
|
payee: invoice.metadata?.extensions?.payee,
|
|
|
|
|
|
|
|
// Tax representative
|
|
|
|
taxRepresentative: invoice.metadata?.extensions?.taxRepresentative,
|
|
|
|
|
|
|
|
// Delivery
|
|
|
|
delivery: this.mapDelivery(invoice),
|
|
|
|
|
|
|
|
// Invoice period
|
2025-08-11 18:55:30 +00:00
|
|
|
invoicingPeriod: invoice.metadata?.extensions?.invoicingPeriod ? {
|
|
|
|
startDate: invoice.metadata.extensions.invoicingPeriod.startDate,
|
|
|
|
endDate: invoice.metadata.extensions.invoicingPeriod.endDate,
|
|
|
|
descriptionCode: invoice.metadata.extensions.invoicingPeriod.descriptionCode
|
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.
2025-08-11 18:07:01 +00:00
|
|
|
} : undefined,
|
|
|
|
|
|
|
|
// Payment instructions
|
|
|
|
paymentInstructions: this.mapPaymentInstructions(invoice),
|
|
|
|
|
|
|
|
// Payment card info
|
|
|
|
paymentCardInfo: invoice.metadata?.extensions?.paymentCard,
|
|
|
|
|
|
|
|
// Direct debit
|
|
|
|
directDebit: invoice.metadata?.extensions?.directDebit,
|
|
|
|
|
|
|
|
// Payment terms
|
|
|
|
paymentTerms: invoice.dueInDays !== undefined ? {
|
|
|
|
note: `Payment due in ${invoice.dueInDays} days`
|
|
|
|
} : undefined,
|
|
|
|
|
|
|
|
// Document level allowances and charges
|
|
|
|
documentLevelAllowances: invoice.metadata?.extensions?.documentAllowances,
|
|
|
|
documentLevelCharges: invoice.metadata?.extensions?.documentCharges,
|
|
|
|
|
|
|
|
// Document totals
|
|
|
|
documentTotals: this.mapDocumentTotals(invoice),
|
|
|
|
|
|
|
|
// VAT breakdown
|
|
|
|
vatBreakdown: this.mapVATBreakdown(invoice),
|
|
|
|
|
|
|
|
// Additional documents
|
|
|
|
additionalDocuments: invoice.metadata?.extensions?.supportingDocuments,
|
|
|
|
|
|
|
|
// Invoice lines
|
|
|
|
invoiceLines: this.mapInvoiceLines(invoice.items || [])
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert EN16931 Semantic Model to EInvoice
|
|
|
|
*/
|
|
|
|
public fromSemanticModel(model: EN16931SemanticModel): EInvoice {
|
|
|
|
const invoice = new EInvoice();
|
|
|
|
invoice.accountingDocId = model.documentInformation.invoiceNumber;
|
|
|
|
invoice.issueDate = model.documentInformation.issueDate;
|
2025-08-11 18:55:30 +00:00
|
|
|
invoice.accountingDocType = this.reverseMapInvoiceType(model.documentInformation.typeCode) as 'invoice';
|
|
|
|
invoice.currency = model.documentInformation.currencyCode as any;
|
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.
2025-08-11 18:07:01 +00:00
|
|
|
invoice.from = this.reverseMapSeller(model.seller);
|
|
|
|
invoice.to = this.reverseMapBuyer(model.buyer);
|
|
|
|
invoice.items = this.reverseMapInvoiceLines(model.invoiceLines);
|
|
|
|
|
|
|
|
// Set metadata
|
|
|
|
if (model.processControl) {
|
|
|
|
invoice.metadata = {
|
|
|
|
...invoice.metadata,
|
|
|
|
profileId: model.processControl.specificationIdentifier,
|
2025-08-11 18:55:30 +00:00
|
|
|
extensions: {
|
|
|
|
...invoice.metadata?.extensions,
|
|
|
|
businessProcessId: model.processControl.businessProcessType
|
|
|
|
}
|
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.
2025-08-11 18:07:01 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set references
|
|
|
|
if (model.references) {
|
|
|
|
invoice.metadata = {
|
|
|
|
...invoice.metadata,
|
|
|
|
buyerReference: model.references.buyerReference,
|
|
|
|
extensions: {
|
|
|
|
...invoice.metadata?.extensions,
|
2025-08-11 18:55:30 +00:00
|
|
|
contractReference: model.references.contractReference,
|
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.
2025-08-11 18:07:01 +00:00
|
|
|
purchaseOrderReference: model.references.purchaseOrderReference,
|
|
|
|
salesOrderReference: model.references.salesOrderReference,
|
2025-08-11 18:55:30 +00:00
|
|
|
precedingInvoices: model.references.precedingInvoices,
|
|
|
|
projectReference: model.references.projectReference
|
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.
2025-08-11 18:07:01 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set payment terms
|
|
|
|
if (model.paymentTerms?.note) {
|
|
|
|
const daysMatch = model.paymentTerms.note.match(/(\d+) days/);
|
|
|
|
if (daysMatch) {
|
|
|
|
invoice.dueInDays = parseInt(daysMatch[1], 10);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set payment options
|
|
|
|
if (model.paymentInstructions.paymentAccountIdentifier) {
|
|
|
|
invoice.paymentOptions = {
|
|
|
|
sepa: {
|
|
|
|
iban: model.paymentInstructions.paymentAccountIdentifier,
|
|
|
|
bic: model.paymentInstructions.paymentServiceProviderIdentifier
|
|
|
|
},
|
|
|
|
bankInfo: {
|
|
|
|
accountHolder: model.paymentInstructions.paymentAccountName || '',
|
|
|
|
institutionName: model.paymentInstructions.paymentServiceProviderIdentifier || ''
|
|
|
|
}
|
|
|
|
} as any;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set extensions
|
|
|
|
if (model.payee || model.taxRepresentative || model.documentLevelAllowances) {
|
|
|
|
invoice.metadata = {
|
|
|
|
...invoice.metadata,
|
|
|
|
extensions: {
|
|
|
|
...invoice.metadata?.extensions,
|
|
|
|
payee: model.payee,
|
|
|
|
taxRepresentative: model.taxRepresentative,
|
|
|
|
documentAllowances: model.documentLevelAllowances,
|
|
|
|
documentCharges: model.documentLevelCharges,
|
|
|
|
supportingDocuments: model.additionalDocuments,
|
|
|
|
paymentCard: model.paymentCardInfo,
|
|
|
|
directDebit: model.directDebit,
|
|
|
|
taxDetails: model.vatBreakdown
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return invoice;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map invoice type code
|
|
|
|
*/
|
|
|
|
private mapInvoiceType(type: string): string {
|
|
|
|
const typeMap: Record<string, string> = {
|
|
|
|
'invoice': '380',
|
|
|
|
'creditNote': '381',
|
|
|
|
'debitNote': '383',
|
|
|
|
'correctedInvoice': '384',
|
|
|
|
'prepaymentInvoice': '386',
|
|
|
|
'selfBilledInvoice': '389',
|
|
|
|
'invoice_380': '380',
|
|
|
|
'credit_note_381': '381'
|
|
|
|
};
|
|
|
|
return typeMap[type] || '380';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reverse map invoice type code
|
|
|
|
*/
|
|
|
|
private reverseMapInvoiceType(code: string): string {
|
|
|
|
const typeMap: Record<string, string> = {
|
|
|
|
'380': 'invoice',
|
|
|
|
'381': 'creditNote',
|
|
|
|
'383': 'debitNote',
|
|
|
|
'384': 'correctedInvoice',
|
|
|
|
'386': 'prepaymentInvoice',
|
|
|
|
'389': 'selfBilledInvoice'
|
|
|
|
};
|
|
|
|
return typeMap[code] || 'invoice';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map notes
|
|
|
|
*/
|
|
|
|
private mapNotes(notes: string | string[]): Array<{ noteContent: string }> {
|
|
|
|
const notesArray = Array.isArray(notes) ? notes : [notes];
|
|
|
|
return notesArray.map(note => ({ noteContent: note }));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map seller information
|
|
|
|
*/
|
|
|
|
private mapSeller(from: EInvoice['from']): Seller {
|
|
|
|
const contact = from as any;
|
|
|
|
if (contact.type === 'company') {
|
|
|
|
return {
|
|
|
|
name: contact.name || '',
|
|
|
|
tradingName: contact.tradingName,
|
|
|
|
identifier: contact.registrationDetails?.registrationId,
|
|
|
|
legalRegistrationIdentifier: contact.registrationDetails?.registrationId,
|
|
|
|
vatIdentifier: contact.registrationDetails?.vatId || contact.vatNumber,
|
|
|
|
taxRegistrationIdentifier: contact.taxId,
|
|
|
|
additionalLegalInfo: contact.description,
|
|
|
|
electronicAddress: contact.email || contact.contact?.email
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return {
|
|
|
|
name: contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(),
|
|
|
|
identifier: contact.registrationDetails?.registrationId,
|
|
|
|
vatIdentifier: contact.registrationDetails?.vatId || contact.vatNumber,
|
|
|
|
electronicAddress: contact.email
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map buyer information
|
|
|
|
*/
|
|
|
|
private mapBuyer(to: EInvoice['to']): Buyer {
|
|
|
|
const contact = to as any;
|
|
|
|
if (contact.type === 'company') {
|
|
|
|
return {
|
|
|
|
name: contact.name || '',
|
|
|
|
tradingName: contact.tradingName,
|
|
|
|
identifier: contact.registrationDetails?.registrationId,
|
|
|
|
legalRegistrationIdentifier: contact.registrationDetails?.registrationId,
|
|
|
|
vatIdentifier: contact.registrationDetails?.vatId || contact.vatNumber,
|
|
|
|
electronicAddress: contact.email || contact.contact?.email
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return {
|
|
|
|
name: contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(),
|
|
|
|
identifier: contact.registrationDetails?.registrationId,
|
|
|
|
vatIdentifier: contact.registrationDetails?.vatId || contact.vatNumber,
|
|
|
|
electronicAddress: contact.email
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map address
|
|
|
|
*/
|
|
|
|
private mapAddress(party: EInvoice['from'] | EInvoice['to']): PostalAddress {
|
|
|
|
const contact = party as any;
|
|
|
|
const address: PostalAddress = {
|
|
|
|
countryCode: contact.address?.country || contact.country || ''
|
|
|
|
};
|
|
|
|
|
|
|
|
if (contact.address) {
|
|
|
|
if (typeof contact.address === 'string') {
|
|
|
|
const addressParts = contact.address.split(',').map((s: string) => s.trim());
|
|
|
|
address.addressLine1 = addressParts[0];
|
|
|
|
if (addressParts.length > 1) address.addressLine2 = addressParts[1];
|
|
|
|
} else if (typeof contact.address === 'object') {
|
|
|
|
address.addressLine1 = [contact.address.streetName, contact.address.houseNumber].filter(Boolean).join(' ');
|
|
|
|
address.city = contact.address.city;
|
|
|
|
address.postCode = contact.address.postalCode;
|
|
|
|
address.countryCode = contact.address.country || address.countryCode;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Support both nested and flat structures
|
|
|
|
if (!address.city) address.city = contact.city;
|
|
|
|
if (!address.postCode) address.postCode = contact.postalCode;
|
|
|
|
|
|
|
|
return address;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map contact information
|
|
|
|
*/
|
|
|
|
private mapContact(party: EInvoice['from'] | EInvoice['to']): Contact | undefined {
|
|
|
|
const contact = party as any;
|
|
|
|
if (contact.type === 'company' && contact.contact) {
|
|
|
|
return {
|
|
|
|
contactPoint: contact.contact.name,
|
|
|
|
telephoneNumber: contact.contact.phone,
|
|
|
|
emailAddress: contact.contact.email
|
|
|
|
};
|
|
|
|
} else if (contact.type === 'person') {
|
|
|
|
return {
|
|
|
|
contactPoint: contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(),
|
|
|
|
telephoneNumber: contact.phone,
|
|
|
|
emailAddress: contact.email
|
|
|
|
};
|
|
|
|
} else if (contact.email || contact.phone) {
|
|
|
|
// Fallback for any contact with email or phone
|
|
|
|
return {
|
|
|
|
contactPoint: contact.name,
|
|
|
|
telephoneNumber: contact.phone,
|
|
|
|
emailAddress: contact.email
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map delivery information
|
|
|
|
*/
|
|
|
|
private mapDelivery(invoice: EInvoice): DeliveryInformation | undefined {
|
|
|
|
const delivery = invoice.metadata?.extensions?.delivery;
|
|
|
|
if (!delivery) return undefined;
|
|
|
|
|
|
|
|
return {
|
|
|
|
name: delivery.name,
|
|
|
|
locationIdentifier: delivery.locationId,
|
|
|
|
actualDeliveryDate: delivery.actualDate,
|
|
|
|
deliveryAddress: delivery.address ? {
|
|
|
|
addressLine1: delivery.address.line1,
|
|
|
|
addressLine2: delivery.address.line2,
|
|
|
|
city: delivery.address.city,
|
|
|
|
postCode: delivery.address.postCode,
|
|
|
|
countryCode: delivery.address.countryCode
|
|
|
|
} : undefined
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map payment instructions
|
|
|
|
*/
|
|
|
|
private mapPaymentInstructions(invoice: EInvoice): PaymentInstructions {
|
|
|
|
const paymentMeans = invoice.metadata?.extensions?.paymentMeans;
|
2025-08-11 18:55:30 +00:00
|
|
|
const paymentAccount = invoice.metadata?.extensions?.paymentAccount;
|
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.
2025-08-11 18:07:01 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
paymentMeansTypeCode: paymentMeans?.paymentMeansCode || '30', // Default to credit transfer
|
|
|
|
paymentMeansText: paymentMeans?.paymentMeansText,
|
|
|
|
remittanceInformation: paymentMeans?.remittanceInformation,
|
2025-08-11 18:55:30 +00:00
|
|
|
paymentAccountIdentifier: paymentAccount?.iban,
|
|
|
|
paymentAccountName: paymentAccount?.accountName,
|
|
|
|
paymentServiceProviderIdentifier: paymentAccount?.bic || paymentAccount?.institutionName
|
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.
2025-08-11 18:07:01 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map document totals
|
|
|
|
*/
|
|
|
|
private mapDocumentTotals(invoice: EInvoice): DocumentTotals {
|
|
|
|
return {
|
|
|
|
lineExtensionAmount: invoice.totalNet,
|
|
|
|
taxExclusiveAmount: invoice.totalNet,
|
|
|
|
taxInclusiveAmount: invoice.totalGross,
|
|
|
|
allowanceTotalAmount: invoice.metadata?.extensions?.documentAllowances?.reduce(
|
|
|
|
(sum, a) => sum + a.amount, 0
|
|
|
|
),
|
|
|
|
chargeTotalAmount: invoice.metadata?.extensions?.documentCharges?.reduce(
|
|
|
|
(sum, c) => sum + c.amount, 0
|
|
|
|
),
|
|
|
|
prepaidAmount: invoice.metadata?.extensions?.prepaidAmount,
|
|
|
|
roundingAmount: invoice.metadata?.extensions?.roundingAmount,
|
|
|
|
payableAmount: invoice.totalGross
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map VAT breakdown
|
|
|
|
*/
|
|
|
|
private mapVATBreakdown(invoice: EInvoice): VATBreakdown[] | undefined {
|
|
|
|
const taxDetails = invoice.metadata?.extensions?.taxDetails;
|
|
|
|
if (!taxDetails) {
|
|
|
|
// Create default VAT breakdown from invoice totals
|
|
|
|
if (invoice.totalVat > 0) {
|
|
|
|
return [{
|
|
|
|
vatCategoryTaxableAmount: invoice.totalNet,
|
|
|
|
vatCategoryTaxAmount: invoice.totalVat,
|
|
|
|
vatCategoryCode: 'S', // Standard rate
|
|
|
|
vatCategoryRate: (invoice.totalVat / invoice.totalNet) * 100
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return taxDetails as VATBreakdown[];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map invoice lines
|
|
|
|
*/
|
|
|
|
private mapInvoiceLines(items: EInvoice['items']): InvoiceLine[] {
|
|
|
|
if (!items) return [];
|
|
|
|
|
|
|
|
return items.map((item, index) => ({
|
|
|
|
identifier: (index + 1).toString(),
|
|
|
|
note: (item as any).description || (item as any).text || '',
|
|
|
|
invoicedQuantity: item.unitQuantity,
|
|
|
|
invoicedQuantityUnitOfMeasureCode: item.unitType || 'C62',
|
|
|
|
lineExtensionAmount: item.unitNetPrice * item.unitQuantity,
|
|
|
|
purchaseOrderLineReference: (item as any).purchaseOrderLineRef,
|
|
|
|
buyerAccountingReference: (item as any).buyerAccountingRef,
|
|
|
|
period: (item as any).period,
|
|
|
|
allowances: (item as any).allowances,
|
|
|
|
charges: (item as any).charges,
|
|
|
|
priceDetails: {
|
|
|
|
itemNetPrice: item.unitNetPrice,
|
|
|
|
itemPriceDiscount: (item as any).priceDiscount,
|
|
|
|
itemGrossPrice: (item as any).grossPrice,
|
|
|
|
itemPriceBaseQuantity: (item as any).priceBaseQuantity || 1
|
|
|
|
},
|
|
|
|
vatInformation: {
|
|
|
|
categoryCode: this.mapVATCategory(item.vatPercentage),
|
|
|
|
rate: item.vatPercentage
|
|
|
|
},
|
|
|
|
itemInformation: {
|
|
|
|
name: item.name,
|
|
|
|
description: (item as any).description || (item as any).text || '',
|
|
|
|
sellersIdentifier: item.articleNumber,
|
|
|
|
buyersIdentifier: (item as any).buyersItemId,
|
|
|
|
standardIdentifier: (item as any).gtin || (item as any).ean,
|
|
|
|
classificationIdentifier: (item as any).unspsc,
|
|
|
|
originCountryCode: (item as any).originCountry,
|
|
|
|
attributes: (item as any).attributes
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map VAT category from percentage
|
|
|
|
*/
|
|
|
|
private mapVATCategory(percentage?: number): string {
|
|
|
|
if (percentage === undefined || percentage === null) return 'S';
|
|
|
|
if (percentage === 0) return 'Z';
|
|
|
|
if (percentage > 0) return 'S';
|
|
|
|
return 'E'; // Exempt
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reverse map seller
|
|
|
|
*/
|
|
|
|
private reverseMapSeller(seller: Seller & { postalAddress: PostalAddress }): EInvoice['from'] {
|
|
|
|
const isCompany = seller.legalRegistrationIdentifier || seller.tradingName;
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: isCompany ? 'company' : 'person',
|
|
|
|
name: seller.name,
|
|
|
|
description: seller.additionalLegalInfo || '',
|
|
|
|
address: {
|
|
|
|
streetName: seller.postalAddress.addressLine1 || '',
|
|
|
|
houseNumber: '',
|
|
|
|
city: seller.postalAddress.city || '',
|
|
|
|
postalCode: seller.postalAddress.postCode || '',
|
|
|
|
country: seller.postalAddress.countryCode || ''
|
|
|
|
},
|
|
|
|
registrationDetails: {
|
|
|
|
vatId: seller.vatIdentifier || '',
|
|
|
|
registrationId: seller.identifier || seller.legalRegistrationIdentifier || '',
|
|
|
|
registrationName: seller.name
|
|
|
|
},
|
|
|
|
status: 'active',
|
|
|
|
foundedDate: {
|
|
|
|
year: new Date().getFullYear(),
|
|
|
|
month: new Date().getMonth() + 1,
|
|
|
|
day: new Date().getDate()
|
|
|
|
}
|
|
|
|
} as any;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reverse map buyer
|
|
|
|
*/
|
|
|
|
private reverseMapBuyer(buyer: Buyer & { postalAddress: PostalAddress }): EInvoice['to'] {
|
|
|
|
const isCompany = buyer.legalRegistrationIdentifier || buyer.tradingName;
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: isCompany ? 'company' : 'person',
|
|
|
|
name: buyer.name,
|
|
|
|
description: '',
|
|
|
|
address: {
|
|
|
|
streetName: buyer.postalAddress.addressLine1 || '',
|
|
|
|
houseNumber: '',
|
|
|
|
city: buyer.postalAddress.city || '',
|
|
|
|
postalCode: buyer.postalAddress.postCode || '',
|
|
|
|
country: buyer.postalAddress.countryCode || ''
|
|
|
|
},
|
|
|
|
registrationDetails: {
|
|
|
|
vatId: buyer.vatIdentifier || '',
|
|
|
|
registrationId: buyer.identifier || buyer.legalRegistrationIdentifier || '',
|
|
|
|
registrationName: buyer.name
|
|
|
|
},
|
|
|
|
status: 'active',
|
|
|
|
foundedDate: {
|
|
|
|
year: new Date().getFullYear(),
|
|
|
|
month: new Date().getMonth() + 1,
|
|
|
|
day: new Date().getDate()
|
|
|
|
}
|
|
|
|
} as any;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reverse map invoice lines
|
|
|
|
*/
|
|
|
|
private reverseMapInvoiceLines(lines: InvoiceLine[]): EInvoice['items'] {
|
|
|
|
return lines.map((line, index) => ({
|
|
|
|
position: index + 1,
|
|
|
|
name: line.itemInformation.name,
|
|
|
|
description: line.itemInformation.description || '',
|
|
|
|
unitQuantity: line.invoicedQuantity,
|
|
|
|
unitType: line.invoicedQuantityUnitOfMeasureCode,
|
|
|
|
unitNetPrice: line.priceDetails.itemNetPrice,
|
|
|
|
vatPercentage: line.vatInformation.rate || 0,
|
|
|
|
articleNumber: line.itemInformation.sellersIdentifier || ''
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validate semantic model completeness
|
|
|
|
*/
|
|
|
|
public validateSemanticModel(model: EN16931SemanticModel): string[] {
|
|
|
|
const errors: string[] = [];
|
|
|
|
|
|
|
|
// Check mandatory fields
|
|
|
|
if (!model.documentInformation.invoiceNumber) {
|
|
|
|
errors.push('BT-1: Invoice number is mandatory');
|
|
|
|
}
|
|
|
|
if (!model.documentInformation.issueDate) {
|
|
|
|
errors.push('BT-2: Invoice issue date is mandatory');
|
|
|
|
}
|
|
|
|
if (!model.documentInformation.typeCode) {
|
|
|
|
errors.push('BT-3: Invoice type code is mandatory');
|
|
|
|
}
|
|
|
|
if (!model.documentInformation.currencyCode) {
|
|
|
|
errors.push('BT-5: Invoice currency code is mandatory');
|
|
|
|
}
|
|
|
|
if (!model.seller?.name) {
|
|
|
|
errors.push('BT-27: Seller name is mandatory');
|
|
|
|
}
|
|
|
|
if (!model.seller?.postalAddress?.countryCode) {
|
|
|
|
errors.push('BT-40: Seller country code is mandatory');
|
|
|
|
}
|
|
|
|
if (!model.buyer?.name) {
|
|
|
|
errors.push('BT-44: Buyer name is mandatory');
|
|
|
|
}
|
|
|
|
if (!model.buyer?.postalAddress?.countryCode) {
|
|
|
|
errors.push('BT-55: Buyer country code is mandatory');
|
|
|
|
}
|
|
|
|
if (!model.documentTotals) {
|
|
|
|
errors.push('BG-22: Document totals are mandatory');
|
|
|
|
}
|
|
|
|
if (!model.invoiceLines || model.invoiceLines.length === 0) {
|
|
|
|
errors.push('BG-25: At least one invoice line is mandatory');
|
|
|
|
}
|
|
|
|
|
|
|
|
return errors;
|
|
|
|
}
|
|
|
|
}
|