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:
524
ts/formats/semantic/bt-bg.model.ts
Normal file
524
ts/formats/semantic/bt-bg.model.ts
Normal 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'];
|
596
ts/formats/semantic/semantic.adapter.ts
Normal file
596
ts/formats/semantic/semantic.adapter.ts
Normal 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;
|
||||
}
|
||||
}
|
654
ts/formats/semantic/semantic.validator.ts
Normal file
654
ts/formats/semantic/semantic.validator.ts
Normal 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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user