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:
2025-08-11 18:07:01 +00:00
parent 10e14af85b
commit cbb297b0b1
24 changed files with 7714 additions and 98 deletions

View File

@@ -0,0 +1,524 @@
/**
* EN16931 Canonical Semantic Model
* Defines all Business Terms (BT) and Business Groups (BG) from the standard
* This provides a format-agnostic representation of invoice data
*/
/**
* Business Term (BT) definitions from EN16931
* Each BT represents a specific data element in an invoice
*/
export interface BusinessTerms {
// Document level information (BT-1 to BT-22)
BT1_InvoiceNumber: string;
BT2_InvoiceIssueDate: Date;
BT3_InvoiceTypeCode: string;
BT4_InvoiceNote?: string;
BT5_InvoiceCurrencyCode: string;
BT6_VATAccountingCurrencyCode?: string;
BT7_ValueDateForVATCalculation?: Date;
BT8_InvoicePeriodDescriptionCode?: string;
BT9_DueDate?: Date;
BT10_BuyerReference?: string;
BT11_ProjectReference?: string;
BT12_ContractReference?: string;
BT13_PurchaseOrderReference?: string;
BT14_SalesOrderReference?: string;
BT15_ReceivingAdviceReference?: string;
BT16_DespatchAdviceReference?: string;
BT17_TenderOrLotReference?: string;
BT18_InvoicedObjectIdentifier?: string;
BT19_BuyerAccountingReference?: string;
BT20_PaymentTerms?: string;
BT21_InvoiceNote?: string[];
BT22_ProcessSpecificNote?: string;
// Seller information (BT-23 to BT-40)
BT23_BusinessProcessType?: string;
BT24_SpecificationIdentifier?: string;
BT25_InvoiceAttachment?: Attachment[];
BT26_InvoiceDocumentReference?: string;
BT27_SellerName: string;
BT28_SellerTradingName?: string;
BT29_SellerIdentifier?: string;
BT30_SellerLegalRegistrationIdentifier?: string;
BT31_SellerVATIdentifier?: string;
BT32_SellerTaxRegistrationIdentifier?: string;
BT33_SellerAdditionalLegalInfo?: string;
BT34_SellerElectronicAddress?: string;
BT35_SellerAddressLine1?: string;
BT36_SellerAddressLine2?: string;
BT37_SellerAddressLine3?: string;
BT38_SellerCity?: string;
BT39_SellerPostCode?: string;
BT40_SellerCountryCode: string;
// Seller contact (BT-41 to BT-43)
BT41_SellerContactPoint?: string;
BT42_SellerContactTelephoneNumber?: string;
BT43_SellerContactEmailAddress?: string;
// Buyer information (BT-44 to BT-58)
BT44_BuyerName: string;
BT45_BuyerTradingName?: string;
BT46_BuyerIdentifier?: string;
BT47_BuyerLegalRegistrationIdentifier?: string;
BT48_BuyerVATIdentifier?: string;
BT49_BuyerElectronicAddress?: string;
BT50_BuyerAddressLine1?: string;
BT51_BuyerAddressLine2?: string;
BT52_BuyerAddressLine3?: string;
BT53_BuyerCity?: string;
BT54_BuyerPostCode?: string;
BT55_BuyerCountryCode: string;
BT56_BuyerContactPoint?: string;
BT57_BuyerContactTelephoneNumber?: string;
BT58_BuyerContactEmailAddress?: string;
// Payee information (BT-59 to BT-62)
BT59_PayeeName?: string;
BT60_PayeeIdentifier?: string;
BT61_PayeeLegalRegistrationIdentifier?: string;
BT62_PayeeLegalRegistrationIdentifierSchemeID?: string;
// Tax representative (BT-62 to BT-69)
BT63_SellerTaxRepresentativeName?: string;
BT64_SellerTaxRepresentativeVATIdentifier?: string;
BT65_SellerTaxRepresentativeAddressLine1?: string;
BT66_SellerTaxRepresentativeAddressLine2?: string;
BT67_SellerTaxRepresentativeCity?: string;
BT68_SellerTaxRepresentativePostCode?: string;
BT69_SellerTaxRepresentativeCountryCode?: string;
// Delivery information (BT-70 to BT-80)
BT70_DeliveryName?: string;
BT71_DeliveryLocationIdentifier?: string;
BT72_ActualDeliveryDate?: Date;
BT73_InvoicingPeriodStartDate?: Date;
BT74_InvoicingPeriodEndDate?: Date;
BT75_DeliveryAddressLine1?: string;
BT76_DeliveryAddressLine2?: string;
BT77_DeliveryAddressLine3?: string;
BT78_DeliveryCity?: string;
BT79_DeliveryPostCode?: string;
BT80_DeliveryCountryCode?: string;
// Payment instructions (BT-81 to BT-91)
BT81_PaymentMeansTypeCode: string;
BT82_PaymentMeansText?: string;
BT83_RemittanceInformation?: string;
BT84_PaymentAccountIdentifier?: string;
BT85_PaymentAccountName?: string;
BT86_PaymentServiceProviderIdentifier?: string;
BT87_PaymentCardAccountPrimaryNumber?: string;
BT88_PaymentCardAccountHolderName?: string;
BT89_MandateReferenceIdentifier?: string;
BT90_BankAssignedCreditorIdentifier?: string;
BT91_DebitedAccountIdentifier?: string;
// Document level allowances (BT-92 to BT-96)
BT92_DocumentLevelAllowanceAmount?: number;
BT93_DocumentLevelAllowanceBaseAmount?: number;
BT94_DocumentLevelAllowancePercentage?: number;
BT95_DocumentLevelAllowanceVATCategoryCode?: string;
BT96_DocumentLevelAllowanceVATRate?: number;
BT97_DocumentLevelAllowanceReason?: string;
BT98_DocumentLevelAllowanceReasonCode?: string;
// Document level charges (BT-99 to BT-105)
BT99_DocumentLevelChargeAmount?: number;
BT100_DocumentLevelChargeBaseAmount?: number;
BT101_DocumentLevelChargePercentage?: number;
BT102_DocumentLevelChargeVATCategoryCode?: string;
BT103_DocumentLevelChargeVATRate?: number;
BT104_DocumentLevelChargeReason?: string;
BT105_DocumentLevelChargeReasonCode?: string;
// Document totals (BT-106 to BT-115)
BT106_SumOfInvoiceLineNetAmount: number;
BT107_SumOfAllowancesOnDocumentLevel?: number;
BT108_SumOfChargesOnDocumentLevel?: number;
BT109_InvoiceTotalAmountWithoutVAT: number;
BT110_InvoiceTotalVATAmount?: number;
BT111_InvoiceTotalVATAmountInAccountingCurrency?: number;
BT112_InvoiceTotalAmountWithVAT: number;
BT113_PaidAmount?: number;
BT114_RoundingAmount?: number;
BT115_AmountDueForPayment: number;
// VAT breakdown (BT-116 to BT-121)
BT116_VATCategoryTaxableAmount?: number;
BT117_VATCategoryTaxAmount?: number;
BT118_VATCategoryCode?: string;
BT119_VATCategoryRate?: number;
BT120_VATExemptionReasonText?: string;
BT121_VATExemptionReasonCode?: string;
// Additional document references (BT-122 to BT-125)
BT122_SupportingDocumentReference?: string;
BT123_SupportingDocumentDescription?: string;
BT124_ExternalDocumentLocation?: string;
BT125_AttachedDocumentEmbedded?: string;
// Line level information (BT-126 to BT-162)
BT126_InvoiceLineIdentifier?: string;
BT127_InvoiceLineNote?: string;
BT128_InvoiceLineObjectIdentifier?: string;
BT129_InvoicedQuantity?: number;
BT130_InvoicedQuantityUnitOfMeasureCode?: string;
BT131_InvoiceLineNetAmount?: number;
BT132_ReferencedPurchaseOrderLineReference?: string;
BT133_InvoiceLineBuyerAccountingReference?: string;
BT134_InvoiceLinePeriodStartDate?: Date;
BT135_InvoiceLinePeriodEndDate?: Date;
BT136_InvoiceLineAllowanceAmount?: number;
BT137_InvoiceLineAllowanceBaseAmount?: number;
BT138_InvoiceLineAllowancePercentage?: number;
BT139_InvoiceLineAllowanceReason?: string;
BT140_InvoiceLineAllowanceReasonCode?: string;
BT141_InvoiceLineChargeAmount?: number;
BT142_InvoiceLineChargeBaseAmount?: number;
BT143_InvoiceLineChargePercentage?: number;
BT144_InvoiceLineChargeReason?: string;
BT145_InvoiceLineChargeReasonCode?: string;
BT146_ItemNetPrice?: number;
BT147_ItemPriceDiscount?: number;
BT148_ItemGrossPrice?: number;
BT149_ItemPriceBaseQuantity?: number;
BT150_ItemPriceBaseQuantityUnitOfMeasureCode?: string;
BT151_ItemVATCategoryCode?: string;
BT152_ItemVATRate?: number;
BT153_ItemName?: string;
BT154_ItemDescription?: string;
BT155_ItemSellersIdentifier?: string;
BT156_ItemBuyersIdentifier?: string;
BT157_ItemStandardIdentifier?: string;
BT158_ItemClassificationIdentifier?: string;
BT159_ItemClassificationListIdentifier?: string;
BT160_ItemOriginCountryCode?: string;
BT161_ItemAttributeName?: string;
BT162_ItemAttributeValue?: string;
}
/**
* Business Groups (BG) from EN16931
* Groups related business terms together
*/
export interface BusinessGroups {
BG1_InvoiceNote?: InvoiceNote;
BG2_ProcessControl?: ProcessControl;
BG3_PrecedingInvoiceReference?: PrecedingInvoiceReference[];
BG4_Seller: Seller;
BG5_SellerPostalAddress: PostalAddress;
BG6_SellerContact?: Contact;
BG7_Buyer: Buyer;
BG8_BuyerPostalAddress: PostalAddress;
BG9_BuyerContact?: Contact;
BG10_Payee?: Payee;
BG11_SellerTaxRepresentative?: TaxRepresentative;
BG12_PayerParty?: PayerParty;
BG13_DeliveryInformation?: DeliveryInformation;
BG14_InvoicingPeriod?: Period;
BG15_DeliverToAddress?: PostalAddress;
BG16_PaymentInstructions: PaymentInstructions;
BG17_PaymentCardInformation?: PaymentCardInformation;
BG18_DirectDebit?: DirectDebit;
BG19_PaymentTerms?: PaymentTerms;
BG20_DocumentLevelAllowances?: Allowance[];
BG21_DocumentLevelCharges?: Charge[];
BG22_DocumentTotals: DocumentTotals;
BG23_VATBreakdown?: VATBreakdown[];
BG24_AdditionalSupportingDocuments?: SupportingDocument[];
BG25_InvoiceLine: InvoiceLine[];
BG26_InvoiceLinePeriod?: Period;
BG27_InvoiceLineAllowances?: Allowance[];
BG28_InvoiceLineCharges?: Charge[];
BG29_PriceDetails?: PriceDetails;
BG30_LineVATInformation: VATInformation;
BG31_ItemInformation: ItemInformation;
BG32_ItemAttributes?: ItemAttribute[];
}
/**
* Supporting types for Business Groups
*/
export interface InvoiceNote {
subjectCode?: string;
noteContent: string;
}
export interface ProcessControl {
businessProcessType?: string;
specificationIdentifier: string;
}
export interface PrecedingInvoiceReference {
referenceNumber: string;
issueDate?: Date;
}
export interface Seller {
name: string;
tradingName?: string;
identifier?: string;
legalRegistrationIdentifier?: string;
vatIdentifier?: string;
taxRegistrationIdentifier?: string;
additionalLegalInfo?: string;
electronicAddress?: string;
}
export interface Buyer {
name: string;
tradingName?: string;
identifier?: string;
legalRegistrationIdentifier?: string;
vatIdentifier?: string;
electronicAddress?: string;
}
export interface PostalAddress {
addressLine1?: string;
addressLine2?: string;
addressLine3?: string;
city?: string;
postCode?: string;
countrySubdivision?: string;
countryCode: string;
}
export interface Contact {
contactPoint?: string;
telephoneNumber?: string;
emailAddress?: string;
}
export interface Payee {
name: string;
identifier?: string;
legalRegistrationIdentifier?: string;
}
export interface TaxRepresentative {
name: string;
vatIdentifier: string;
postalAddress: PostalAddress;
}
export interface PayerParty {
name: string;
identifier?: string;
legalRegistrationIdentifier?: string;
}
export interface DeliveryInformation {
name?: string;
locationIdentifier?: string;
actualDeliveryDate?: Date;
deliveryAddress?: PostalAddress;
}
export interface Period {
startDate?: Date;
endDate?: Date;
descriptionCode?: string;
}
export interface PaymentInstructions {
paymentMeansTypeCode: string;
paymentMeansText?: string;
remittanceInformation?: string;
paymentAccountIdentifier?: string;
paymentAccountName?: string;
paymentServiceProviderIdentifier?: string;
}
export interface PaymentCardInformation {
primaryAccountNumber: string;
holderName?: string;
}
export interface DirectDebit {
mandateReferenceIdentifier?: string;
bankAssignedCreditorIdentifier?: string;
debitedAccountIdentifier?: string;
}
export interface PaymentTerms {
note?: string;
}
export interface Allowance {
amount: number;
baseAmount?: number;
percentage?: number;
vatCategoryCode?: string;
vatRate?: number;
reason?: string;
reasonCode?: string;
}
export interface Charge {
amount: number;
baseAmount?: number;
percentage?: number;
vatCategoryCode?: string;
vatRate?: number;
reason?: string;
reasonCode?: string;
}
export interface DocumentTotals {
lineExtensionAmount: number;
taxExclusiveAmount: number;
taxInclusiveAmount: number;
allowanceTotalAmount?: number;
chargeTotalAmount?: number;
prepaidAmount?: number;
roundingAmount?: number;
payableAmount: number;
}
export interface VATBreakdown {
vatCategoryTaxableAmount: number;
vatCategoryTaxAmount: number;
vatCategoryCode: string;
vatCategoryRate?: number;
vatExemptionReasonText?: string;
vatExemptionReasonCode?: string;
}
export interface SupportingDocument {
documentReference: string;
documentDescription?: string;
externalDocumentLocation?: string;
attachedDocument?: Attachment;
}
export interface Attachment {
filename?: string;
mimeType?: string;
description?: string;
embeddedDocumentBinaryObject?: string;
externalDocumentURI?: string;
}
export interface InvoiceLine {
identifier: string;
note?: string;
objectIdentifier?: string;
invoicedQuantity: number;
invoicedQuantityUnitOfMeasureCode: string;
lineExtensionAmount: number;
purchaseOrderLineReference?: string;
buyerAccountingReference?: string;
period?: Period;
allowances?: Allowance[];
charges?: Charge[];
priceDetails: PriceDetails;
vatInformation: VATInformation;
itemInformation: ItemInformation;
}
export interface PriceDetails {
itemNetPrice: number;
itemPriceDiscount?: number;
itemGrossPrice?: number;
itemPriceBaseQuantity?: number;
itemPriceBaseQuantityUnitOfMeasureCode?: string;
}
export interface VATInformation {
categoryCode: string;
rate?: number;
}
export interface ItemInformation {
name: string;
description?: string;
sellersIdentifier?: string;
buyersIdentifier?: string;
standardIdentifier?: string;
classificationIdentifier?: string;
classificationListIdentifier?: string;
originCountryCode?: string;
attributes?: ItemAttribute[];
}
export interface ItemAttribute {
name: string;
value: string;
}
/**
* Complete EN16931 Semantic Model
* Combines all Business Terms and Business Groups
*/
export interface EN16931SemanticModel {
// Core document information
documentInformation: {
invoiceNumber: string; // BT-1
issueDate: Date; // BT-2
typeCode: string; // BT-3
currencyCode: string; // BT-5
notes?: InvoiceNote[]; // BG-1
};
// Process metadata
processControl?: ProcessControl; // BG-2
// References
references?: {
buyerReference?: string; // BT-10
projectReference?: string; // BT-11
contractReference?: string; // BT-12
purchaseOrderReference?: string; // BT-13
salesOrderReference?: string; // BT-14
precedingInvoices?: PrecedingInvoiceReference[]; // BG-3
};
// Parties
seller: Seller & { // BG-4
postalAddress: PostalAddress; // BG-5
contact?: Contact; // BG-6
};
buyer: Buyer & { // BG-7
postalAddress: PostalAddress; // BG-8
contact?: Contact; // BG-9
};
payee?: Payee; // BG-10
taxRepresentative?: TaxRepresentative; // BG-11
// Delivery
delivery?: DeliveryInformation; // BG-13
invoicingPeriod?: Period; // BG-14
// Payment
paymentInstructions: PaymentInstructions; // BG-16
paymentCardInfo?: PaymentCardInformation; // BG-17
directDebit?: DirectDebit; // BG-18
paymentTerms?: PaymentTerms; // BG-19
// Allowances and charges
documentLevelAllowances?: Allowance[]; // BG-20
documentLevelCharges?: Charge[]; // BG-21
// Totals
documentTotals: DocumentTotals; // BG-22
vatBreakdown?: VATBreakdown[]; // BG-23
// Supporting documents
additionalDocuments?: SupportingDocument[]; // BG-24
// Invoice lines
invoiceLines: InvoiceLine[]; // BG-25
}
/**
* Semantic model version and metadata
*/
export const SEMANTIC_MODEL_VERSION = '1.3.0';
export const EN16931_VERSION = '1.3.14';
export const SUPPORTED_SYNTAXES = ['UBL', 'CII', 'EDIFACT'];

View File

@@ -0,0 +1,596 @@
/**
* 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 ? {
businessProcessType: invoice.metadata.businessProcessId,
specificationIdentifier: invoice.metadata.profileId
} : undefined,
// References
references: {
buyerReference: invoice.metadata?.buyerReference,
projectReference: invoice.projectReference,
contractReference: invoice.metadata?.contractReference,
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
invoicingPeriod: invoice.metadata?.invoicingPeriod ? {
startDate: invoice.metadata.invoicingPeriod.startDate,
endDate: invoice.metadata.invoicingPeriod.endDate,
descriptionCode: invoice.metadata.invoicingPeriod.descriptionCode
} : 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;
invoice.accountingDocType = this.reverseMapInvoiceType(model.documentInformation.typeCode);
invoice.currency = model.documentInformation.currencyCode;
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,
businessProcessId: model.processControl.businessProcessType
};
}
// Set references
if (model.references) {
invoice.metadata = {
...invoice.metadata,
buyerReference: model.references.buyerReference,
contractReference: model.references.contractReference,
extensions: {
...invoice.metadata?.extensions,
purchaseOrderReference: model.references.purchaseOrderReference,
salesOrderReference: model.references.salesOrderReference,
precedingInvoices: model.references.precedingInvoices
}
};
invoice.projectReference = model.references.projectReference;
}
// 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;
return {
paymentMeansTypeCode: paymentMeans?.paymentMeansCode || '30', // Default to credit transfer
paymentMeansText: paymentMeans?.paymentMeansText,
remittanceInformation: paymentMeans?.remittanceInformation,
paymentAccountIdentifier: invoice.paymentAccount?.iban,
paymentAccountName: invoice.paymentAccount?.accountName,
paymentServiceProviderIdentifier: invoice.paymentAccount?.bic || invoice.paymentAccount?.institutionName
};
}
/**
* 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;
}
}

View File

@@ -0,0 +1,654 @@
/**
* Semantic Model Validator
* Validates invoices against EN16931 Business Terms and Business Groups
*/
import type { ValidationResult } from '../validation/validation.types.js';
import type { EN16931SemanticModel, BusinessTerms, BusinessGroups } from './bt-bg.model.js';
import type { EInvoice } from '../../einvoice.js';
import { SemanticModelAdapter } from './semantic.adapter.js';
/**
* Business Term validation rules
*/
interface BTValidationRule {
btId: string;
description: string;
mandatory: boolean;
validate: (model: EN16931SemanticModel) => ValidationResult | null;
}
/**
* Semantic Model Validator
* Validates against all EN16931 Business Terms (BT) and Business Groups (BG)
*/
export class SemanticModelValidator {
private adapter: SemanticModelAdapter;
private btRules: BTValidationRule[];
constructor() {
this.adapter = new SemanticModelAdapter();
this.btRules = this.initializeBusinessTermRules();
}
/**
* Validate an invoice using the semantic model
*/
public validate(invoice: EInvoice): ValidationResult[] {
const results: ValidationResult[] = [];
// Convert to semantic model
const model = this.adapter.toSemanticModel(invoice);
// Validate all business terms
for (const rule of this.btRules) {
const result = rule.validate(model);
if (result) {
results.push(result);
}
}
// Validate business groups
results.push(...this.validateBusinessGroups(model));
// Validate cardinality constraints
results.push(...this.validateCardinality(model));
// Validate conditional rules
results.push(...this.validateConditionalRules(model));
return results;
}
/**
* Initialize Business Term validation rules
*/
private initializeBusinessTermRules(): BTValidationRule[] {
return [
// Document level mandatory fields
{
btId: 'BT-1',
description: 'Invoice number',
mandatory: true,
validate: (model) => {
if (!model.documentInformation.invoiceNumber) {
return {
ruleId: 'BT-1',
severity: 'error',
message: 'Invoice number is mandatory',
field: 'documentInformation.invoiceNumber',
btReference: 'BT-1',
source: 'SEMANTIC'
};
}
return null;
}
},
{
btId: 'BT-2',
description: 'Invoice issue date',
mandatory: true,
validate: (model) => {
if (!model.documentInformation.issueDate) {
return {
ruleId: 'BT-2',
severity: 'error',
message: 'Invoice issue date is mandatory',
field: 'documentInformation.issueDate',
btReference: 'BT-2',
source: 'SEMANTIC'
};
}
return null;
}
},
{
btId: 'BT-3',
description: 'Invoice type code',
mandatory: true,
validate: (model) => {
if (!model.documentInformation.typeCode) {
return {
ruleId: 'BT-3',
severity: 'error',
message: 'Invoice type code is mandatory',
field: 'documentInformation.typeCode',
btReference: 'BT-3',
source: 'SEMANTIC'
};
}
const validCodes = ['380', '381', '383', '384', '386', '389'];
if (!validCodes.includes(model.documentInformation.typeCode)) {
return {
ruleId: 'BT-3',
severity: 'error',
message: `Invalid invoice type code. Must be one of: ${validCodes.join(', ')}`,
field: 'documentInformation.typeCode',
value: model.documentInformation.typeCode,
btReference: 'BT-3',
source: 'SEMANTIC'
};
}
return null;
}
},
{
btId: 'BT-5',
description: 'Invoice currency code',
mandatory: true,
validate: (model) => {
if (!model.documentInformation.currencyCode) {
return {
ruleId: 'BT-5',
severity: 'error',
message: 'Invoice currency code is mandatory',
field: 'documentInformation.currencyCode',
btReference: 'BT-5',
source: 'SEMANTIC'
};
}
// Validate ISO 4217 currency code
if (!/^[A-Z]{3}$/.test(model.documentInformation.currencyCode)) {
return {
ruleId: 'BT-5',
severity: 'error',
message: 'Currency code must be a valid ISO 4217 code',
field: 'documentInformation.currencyCode',
value: model.documentInformation.currencyCode,
btReference: 'BT-5',
source: 'SEMANTIC'
};
}
return null;
}
},
// Seller mandatory fields
{
btId: 'BT-27',
description: 'Seller name',
mandatory: true,
validate: (model) => {
if (!model.seller?.name) {
return {
ruleId: 'BT-27',
severity: 'error',
message: 'Seller name is mandatory',
field: 'seller.name',
btReference: 'BT-27',
source: 'SEMANTIC'
};
}
return null;
}
},
{
btId: 'BT-40',
description: 'Seller country code',
mandatory: true,
validate: (model) => {
if (!model.seller?.postalAddress?.countryCode) {
return {
ruleId: 'BT-40',
severity: 'error',
message: 'Seller country code is mandatory',
field: 'seller.postalAddress.countryCode',
btReference: 'BT-40',
source: 'SEMANTIC'
};
}
// Validate ISO 3166-1 alpha-2 country code
if (!/^[A-Z]{2}$/.test(model.seller.postalAddress.countryCode)) {
return {
ruleId: 'BT-40',
severity: 'error',
message: 'Country code must be a valid ISO 3166-1 alpha-2 code',
field: 'seller.postalAddress.countryCode',
value: model.seller.postalAddress.countryCode,
btReference: 'BT-40',
source: 'SEMANTIC'
};
}
return null;
}
},
// Buyer mandatory fields
{
btId: 'BT-44',
description: 'Buyer name',
mandatory: true,
validate: (model) => {
if (!model.buyer?.name) {
return {
ruleId: 'BT-44',
severity: 'error',
message: 'Buyer name is mandatory',
field: 'buyer.name',
btReference: 'BT-44',
source: 'SEMANTIC'
};
}
return null;
}
},
{
btId: 'BT-55',
description: 'Buyer country code',
mandatory: true,
validate: (model) => {
if (!model.buyer?.postalAddress?.countryCode) {
return {
ruleId: 'BT-55',
severity: 'error',
message: 'Buyer country code is mandatory',
field: 'buyer.postalAddress.countryCode',
btReference: 'BT-55',
source: 'SEMANTIC'
};
}
// Validate ISO 3166-1 alpha-2 country code
if (!/^[A-Z]{2}$/.test(model.buyer.postalAddress.countryCode)) {
return {
ruleId: 'BT-55',
severity: 'error',
message: 'Country code must be a valid ISO 3166-1 alpha-2 code',
field: 'buyer.postalAddress.countryCode',
value: model.buyer.postalAddress.countryCode,
btReference: 'BT-55',
source: 'SEMANTIC'
};
}
return null;
}
},
// Payment means
{
btId: 'BT-81',
description: 'Payment means type code',
mandatory: true,
validate: (model) => {
if (!model.paymentInstructions?.paymentMeansTypeCode) {
return {
ruleId: 'BT-81',
severity: 'error',
message: 'Payment means type code is mandatory',
field: 'paymentInstructions.paymentMeansTypeCode',
btReference: 'BT-81',
source: 'SEMANTIC'
};
}
return null;
}
},
// Document totals
{
btId: 'BT-106',
description: 'Sum of invoice line net amount',
mandatory: true,
validate: (model) => {
if (model.documentTotals?.lineExtensionAmount === undefined) {
return {
ruleId: 'BT-106',
severity: 'error',
message: 'Sum of invoice line net amount is mandatory',
field: 'documentTotals.lineExtensionAmount',
btReference: 'BT-106',
source: 'SEMANTIC'
};
}
return null;
}
},
{
btId: 'BT-109',
description: 'Invoice total amount without VAT',
mandatory: true,
validate: (model) => {
if (model.documentTotals?.taxExclusiveAmount === undefined) {
return {
ruleId: 'BT-109',
severity: 'error',
message: 'Invoice total amount without VAT is mandatory',
field: 'documentTotals.taxExclusiveAmount',
btReference: 'BT-109',
source: 'SEMANTIC'
};
}
return null;
}
},
{
btId: 'BT-112',
description: 'Invoice total amount with VAT',
mandatory: true,
validate: (model) => {
if (model.documentTotals?.taxInclusiveAmount === undefined) {
return {
ruleId: 'BT-112',
severity: 'error',
message: 'Invoice total amount with VAT is mandatory',
field: 'documentTotals.taxInclusiveAmount',
btReference: 'BT-112',
source: 'SEMANTIC'
};
}
return null;
}
},
{
btId: 'BT-115',
description: 'Amount due for payment',
mandatory: true,
validate: (model) => {
if (model.documentTotals?.payableAmount === undefined) {
return {
ruleId: 'BT-115',
severity: 'error',
message: 'Amount due for payment is mandatory',
field: 'documentTotals.payableAmount',
btReference: 'BT-115',
source: 'SEMANTIC'
};
}
return null;
}
}
];
}
/**
* Validate Business Groups
*/
private validateBusinessGroups(model: EN16931SemanticModel): ValidationResult[] {
const results: ValidationResult[] = [];
// BG-4: Seller
if (!model.seller) {
results.push({
ruleId: 'BG-4',
severity: 'error',
message: 'Seller information is mandatory',
field: 'seller',
bgReference: 'BG-4',
source: 'SEMANTIC'
});
}
// BG-5: Seller postal address
if (!model.seller?.postalAddress) {
results.push({
ruleId: 'BG-5',
severity: 'error',
message: 'Seller postal address is mandatory',
field: 'seller.postalAddress',
bgReference: 'BG-5',
source: 'SEMANTIC'
});
}
// BG-7: Buyer
if (!model.buyer) {
results.push({
ruleId: 'BG-7',
severity: 'error',
message: 'Buyer information is mandatory',
field: 'buyer',
bgReference: 'BG-7',
source: 'SEMANTIC'
});
}
// BG-8: Buyer postal address
if (!model.buyer?.postalAddress) {
results.push({
ruleId: 'BG-8',
severity: 'error',
message: 'Buyer postal address is mandatory',
field: 'buyer.postalAddress',
bgReference: 'BG-8',
source: 'SEMANTIC'
});
}
// BG-16: Payment instructions
if (!model.paymentInstructions) {
results.push({
ruleId: 'BG-16',
severity: 'error',
message: 'Payment instructions are mandatory',
field: 'paymentInstructions',
bgReference: 'BG-16',
source: 'SEMANTIC'
});
}
// BG-22: Document totals
if (!model.documentTotals) {
results.push({
ruleId: 'BG-22',
severity: 'error',
message: 'Document totals are mandatory',
field: 'documentTotals',
bgReference: 'BG-22',
source: 'SEMANTIC'
});
}
// BG-25: Invoice lines
if (!model.invoiceLines || model.invoiceLines.length === 0) {
results.push({
ruleId: 'BG-25',
severity: 'error',
message: 'At least one invoice line is mandatory',
field: 'invoiceLines',
bgReference: 'BG-25',
source: 'SEMANTIC'
});
}
// Validate each invoice line
model.invoiceLines?.forEach((line, index) => {
// BT-126: Line identifier
if (!line.identifier) {
results.push({
ruleId: 'BT-126',
severity: 'error',
message: `Invoice line ${index + 1}: Identifier is mandatory`,
field: `invoiceLines[${index}].identifier`,
btReference: 'BT-126',
source: 'SEMANTIC'
});
}
// BT-129: Invoiced quantity
if (line.invoicedQuantity === undefined) {
results.push({
ruleId: 'BT-129',
severity: 'error',
message: `Invoice line ${index + 1}: Invoiced quantity is mandatory`,
field: `invoiceLines[${index}].invoicedQuantity`,
btReference: 'BT-129',
source: 'SEMANTIC'
});
}
// BT-131: Line net amount
if (line.lineExtensionAmount === undefined) {
results.push({
ruleId: 'BT-131',
severity: 'error',
message: `Invoice line ${index + 1}: Line net amount is mandatory`,
field: `invoiceLines[${index}].lineExtensionAmount`,
btReference: 'BT-131',
source: 'SEMANTIC'
});
}
// BT-153: Item name
if (!line.itemInformation?.name) {
results.push({
ruleId: 'BT-153',
severity: 'error',
message: `Invoice line ${index + 1}: Item name is mandatory`,
field: `invoiceLines[${index}].itemInformation.name`,
btReference: 'BT-153',
source: 'SEMANTIC'
});
}
});
return results;
}
/**
* Validate cardinality constraints
*/
private validateCardinality(model: EN16931SemanticModel): ValidationResult[] {
const results: ValidationResult[] = [];
// Check for duplicate invoice lines
const lineIds = model.invoiceLines?.map(l => l.identifier) || [];
const uniqueIds = new Set(lineIds);
if (lineIds.length !== uniqueIds.size) {
results.push({
ruleId: 'CARD-01',
severity: 'error',
message: 'Invoice line identifiers must be unique',
field: 'invoiceLines',
source: 'SEMANTIC'
});
}
// Check VAT breakdown cardinality
if (model.vatBreakdown) {
const vatCategories = model.vatBreakdown.map(v => v.vatCategoryCode);
const uniqueCategories = new Set(vatCategories);
if (vatCategories.length !== uniqueCategories.size) {
results.push({
ruleId: 'CARD-02',
severity: 'error',
message: 'Each VAT category code must appear only once in VAT breakdown',
field: 'vatBreakdown',
source: 'SEMANTIC'
});
}
}
return results;
}
/**
* Validate conditional rules
*/
private validateConditionalRules(model: EN16931SemanticModel): ValidationResult[] {
const results: ValidationResult[] = [];
// If VAT accounting currency code is present, VAT amount in accounting currency must be present
if (model.documentInformation.currencyCode !== model.documentInformation.currencyCode) {
if (!model.documentTotals?.taxInclusiveAmount) {
results.push({
ruleId: 'COND-01',
severity: 'error',
message: 'When VAT accounting currency differs from invoice currency, VAT amount in accounting currency is mandatory',
field: 'documentTotals.taxInclusiveAmount',
source: 'SEMANTIC'
});
}
}
// If credit note, there should be a preceding invoice reference
if (model.documentInformation.typeCode === '381') {
if (!model.references?.precedingInvoices || model.references.precedingInvoices.length === 0) {
results.push({
ruleId: 'COND-02',
severity: 'warning',
message: 'Credit notes should reference the original invoice',
field: 'references.precedingInvoices',
source: 'SEMANTIC'
});
}
}
// If tax representative is present, certain fields are mandatory
if (model.taxRepresentative) {
if (!model.taxRepresentative.vatIdentifier) {
results.push({
ruleId: 'COND-03',
severity: 'error',
message: 'Tax representative VAT identifier is mandatory when tax representative is present',
field: 'taxRepresentative.vatIdentifier',
source: 'SEMANTIC'
});
}
}
// VAT exemption requires exemption reason
if (model.vatBreakdown) {
for (const vat of model.vatBreakdown) {
if (vat.vatCategoryCode === 'E' && !vat.vatExemptionReasonText && !vat.vatExemptionReasonCode) {
results.push({
ruleId: 'COND-04',
severity: 'error',
message: 'VAT exemption requires exemption reason text or code',
field: 'vatBreakdown.vatExemptionReasonText',
source: 'SEMANTIC'
});
}
}
}
return results;
}
/**
* Get semantic model from invoice
*/
public getSemanticModel(invoice: EInvoice): EN16931SemanticModel {
return this.adapter.toSemanticModel(invoice);
}
/**
* Create invoice from semantic model
*/
public createInvoice(model: EN16931SemanticModel): EInvoice {
return this.adapter.fromSemanticModel(model);
}
/**
* Get BT/BG mapping for an invoice
*/
public getBusinessTermMapping(invoice: EInvoice): Map<string, any> {
const model = this.adapter.toSemanticModel(invoice);
const mapping = new Map<string, any>();
// Map all business terms
mapping.set('BT-1', model.documentInformation.invoiceNumber);
mapping.set('BT-2', model.documentInformation.issueDate);
mapping.set('BT-3', model.documentInformation.typeCode);
mapping.set('BT-5', model.documentInformation.currencyCode);
mapping.set('BT-10', model.references?.buyerReference);
mapping.set('BT-27', model.seller?.name);
mapping.set('BT-40', model.seller?.postalAddress?.countryCode);
mapping.set('BT-44', model.buyer?.name);
mapping.set('BT-55', model.buyer?.postalAddress?.countryCode);
mapping.set('BT-81', model.paymentInstructions?.paymentMeansTypeCode);
mapping.set('BT-106', model.documentTotals?.lineExtensionAmount);
mapping.set('BT-109', model.documentTotals?.taxExclusiveAmount);
mapping.set('BT-112', model.documentTotals?.taxInclusiveAmount);
mapping.set('BT-115', model.documentTotals?.payableAmount);
// Map business groups
mapping.set('BG-4', model.seller);
mapping.set('BG-5', model.seller?.postalAddress);
mapping.set('BG-7', model.buyer);
mapping.set('BG-8', model.buyer?.postalAddress);
mapping.set('BG-16', model.paymentInstructions);
mapping.set('BG-22', model.documentTotals);
mapping.set('BG-25', model.invoiceLines);
return mapping;
}
}