feat(compliance): improve compliance

This commit is contained in:
2025-05-26 13:33:21 +00:00
parent e7c3a774a3
commit 26deb14893
13 changed files with 3520 additions and 2818 deletions

View File

@ -109,6 +109,9 @@ export class UBLEncoder extends UBLBaseEncoder {
// Add line items
this.addInvoiceLines(doc, root, invoice);
// Preserve metadata if available
this.preserveMetadata(doc, root, invoice);
}
/**
@ -516,4 +519,402 @@ export class UBLEncoder extends UBLBaseEncoder {
if (!countryName) return 'XX';
return countryName.length >= 2 ? countryName.substring(0, 2).toUpperCase() : 'XX';
}
/**
* Preserves metadata from invoice to enhance UBL XML output
* @param doc XML document
* @param root Root element
* @param invoice Invoice data
*/
private preserveMetadata(doc: Document, root: Element, invoice: TInvoice): void {
// Extract metadata if available
const metadata = (invoice as any).metadata?.extensions;
if (!metadata) return;
// Preserve business references
this.addBusinessReferencesToUBL(doc, root, metadata.businessReferences);
// Preserve payment information
this.enhancePaymentInformationUBL(doc, root, metadata.paymentInformation);
// Preserve date information
this.addDateInformationUBL(doc, root, metadata.dateInformation);
// Enhance party information with contact details
this.enhancePartyInformationUBL(doc, invoice);
// Enhance line items with metadata
this.enhanceLineItemsUBL(doc, invoice);
}
/**
* Adds business references from metadata to UBL document
* @param doc XML document
* @param root Root element
* @param businessReferences Business references from metadata
*/
private addBusinessReferencesToUBL(doc: Document, root: Element, businessReferences?: any): void {
if (!businessReferences) return;
// Add OrderReference
if (businessReferences.orderReference && !root.getElementsByTagName('cac:OrderReference')[0]) {
const orderRef = doc.createElement('cac:OrderReference');
const orderId = doc.createElement('cbc:ID');
orderId.textContent = businessReferences.orderReference;
orderRef.appendChild(orderId);
// Insert after DocumentCurrencyCode
const currencyCode = root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
if (currencyCode && currencyCode.parentNode) {
currencyCode.parentNode.insertBefore(orderRef, currencyCode.nextSibling);
}
}
// Add ContractDocumentReference
if (businessReferences.contractReference && !root.getElementsByTagName('cac:ContractDocumentReference')[0]) {
const contractRef = doc.createElement('cac:ContractDocumentReference');
const contractId = doc.createElement('cbc:ID');
contractId.textContent = businessReferences.contractReference;
contractRef.appendChild(contractId);
// Insert after OrderReference or DocumentCurrencyCode
const orderRef = root.getElementsByTagName('cac:OrderReference')[0];
const insertAfter = orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
if (insertAfter && insertAfter.parentNode) {
insertAfter.parentNode.insertBefore(contractRef, insertAfter.nextSibling);
}
}
// Add ProjectReference
if (businessReferences.projectReference && !root.getElementsByTagName('cac:ProjectReference')[0]) {
const projectRef = doc.createElement('cac:ProjectReference');
const projectId = doc.createElement('cbc:ID');
projectId.textContent = businessReferences.projectReference;
projectRef.appendChild(projectId);
// Insert after ContractDocumentReference or other refs
const contractRef = root.getElementsByTagName('cac:ContractDocumentReference')[0];
const orderRef = root.getElementsByTagName('cac:OrderReference')[0];
const insertAfter = contractRef || orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
if (insertAfter && insertAfter.parentNode) {
insertAfter.parentNode.insertBefore(projectRef, insertAfter.nextSibling);
}
}
}
/**
* Enhances payment information from metadata in UBL document
* @param doc XML document
* @param root Root element
* @param paymentInfo Payment information from metadata
*/
private enhancePaymentInformationUBL(doc: Document, root: Element, paymentInfo?: any): void {
if (!paymentInfo) return;
let paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
// Create PaymentMeans if it doesn't exist
if (!paymentMeans) {
paymentMeans = doc.createElement('cac:PaymentMeans');
// Insert before TaxTotal
const taxTotal = root.getElementsByTagName('cac:TaxTotal')[0];
if (taxTotal && taxTotal.parentNode) {
taxTotal.parentNode.insertBefore(paymentMeans, taxTotal);
}
}
// Add PaymentMeansCode
if (paymentInfo.paymentMeansCode && !paymentMeans.getElementsByTagName('cbc:PaymentMeansCode')[0]) {
const meansCode = doc.createElement('cbc:PaymentMeansCode');
meansCode.textContent = paymentInfo.paymentMeansCode;
paymentMeans.appendChild(meansCode);
}
// Add PaymentID
if (paymentInfo.paymentID && !paymentMeans.getElementsByTagName('cbc:PaymentID')[0]) {
const paymentId = doc.createElement('cbc:PaymentID');
paymentId.textContent = paymentInfo.paymentID;
paymentMeans.appendChild(paymentId);
}
// Add IBAN and BIC
if (paymentInfo.iban || paymentInfo.bic) {
let payeeAccount = paymentMeans.getElementsByTagName('cac:PayeeFinancialAccount')[0];
if (!payeeAccount) {
payeeAccount = doc.createElement('cac:PayeeFinancialAccount');
paymentMeans.appendChild(payeeAccount);
}
// Add IBAN
if (paymentInfo.iban && !payeeAccount.getElementsByTagName('cbc:ID')[0]) {
const iban = doc.createElement('cbc:ID');
iban.textContent = paymentInfo.iban;
payeeAccount.appendChild(iban);
}
// Add BIC
if (paymentInfo.bic) {
let finInstBranch = payeeAccount.getElementsByTagName('cac:FinancialInstitutionBranch')[0];
if (!finInstBranch) {
finInstBranch = doc.createElement('cac:FinancialInstitutionBranch');
payeeAccount.appendChild(finInstBranch);
}
let finInst = finInstBranch.getElementsByTagName('cac:FinancialInstitution')[0];
if (!finInst) {
finInst = doc.createElement('cac:FinancialInstitution');
finInstBranch.appendChild(finInst);
}
if (!finInst.getElementsByTagName('cbc:ID')[0]) {
const bic = doc.createElement('cbc:ID');
bic.textContent = paymentInfo.bic;
finInst.appendChild(bic);
}
}
// Add account name
if (paymentInfo.accountName && !payeeAccount.getElementsByTagName('cbc:Name')[0]) {
const accountName = doc.createElement('cbc:Name');
accountName.textContent = paymentInfo.accountName;
// Insert after ID
const id = payeeAccount.getElementsByTagName('cbc:ID')[0];
if (id && id.nextSibling) {
payeeAccount.insertBefore(accountName, id.nextSibling);
} else {
payeeAccount.appendChild(accountName);
}
}
}
// Add payment terms with discount if available
if (paymentInfo.paymentTermsNote && paymentInfo.paymentTermsNote.includes('early payment')) {
let paymentTerms = root.getElementsByTagName('cac:PaymentTerms')[0];
if (!paymentTerms) {
paymentTerms = doc.createElement('cac:PaymentTerms');
// Insert before PaymentMeans
const paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
if (paymentMeans && paymentMeans.parentNode) {
paymentMeans.parentNode.insertBefore(paymentTerms, paymentMeans);
}
}
// Update or add note
let note = paymentTerms.getElementsByTagName('cbc:Note')[0];
if (!note) {
note = doc.createElement('cbc:Note');
paymentTerms.appendChild(note);
}
note.textContent = paymentInfo.paymentTermsNote;
// Add discount percent if available
if (paymentInfo.discountPercent && !paymentTerms.getElementsByTagName('cbc:SettlementDiscountPercent')[0]) {
const discountElement = doc.createElement('cbc:SettlementDiscountPercent');
discountElement.textContent = paymentInfo.discountPercent;
paymentTerms.appendChild(discountElement);
}
}
}
/**
* Adds date information from metadata to UBL document
* @param doc XML document
* @param root Root element
* @param dateInfo Date information from metadata
*/
private addDateInformationUBL(doc: Document, root: Element, dateInfo?: any): void {
if (!dateInfo) return;
// Add InvoicePeriod
if ((dateInfo.periodStart || dateInfo.periodEnd) && !root.getElementsByTagName('cac:InvoicePeriod')[0]) {
const invoicePeriod = doc.createElement('cac:InvoicePeriod');
if (dateInfo.periodStart) {
const startDate = doc.createElement('cbc:StartDate');
startDate.textContent = dateInfo.periodStart;
invoicePeriod.appendChild(startDate);
}
if (dateInfo.periodEnd) {
const endDate = doc.createElement('cbc:EndDate');
endDate.textContent = dateInfo.periodEnd;
invoicePeriod.appendChild(endDate);
}
// Insert after business references or DocumentCurrencyCode
const projectRef = root.getElementsByTagName('cac:ProjectReference')[0];
const contractRef = root.getElementsByTagName('cac:ContractDocumentReference')[0];
const orderRef = root.getElementsByTagName('cac:OrderReference')[0];
const insertAfter = projectRef || contractRef || orderRef || root.getElementsByTagName('cbc:DocumentCurrencyCode')[0];
if (insertAfter && insertAfter.parentNode) {
insertAfter.parentNode.insertBefore(invoicePeriod, insertAfter.nextSibling);
}
}
// Add Delivery with ActualDeliveryDate
if (dateInfo.deliveryDate && !root.getElementsByTagName('cac:Delivery')[0]) {
const delivery = doc.createElement('cac:Delivery');
const deliveryDate = doc.createElement('cbc:ActualDeliveryDate');
deliveryDate.textContent = dateInfo.deliveryDate;
delivery.appendChild(deliveryDate);
// Insert before PaymentMeans
const paymentMeans = root.getElementsByTagName('cac:PaymentMeans')[0];
if (paymentMeans && paymentMeans.parentNode) {
paymentMeans.parentNode.insertBefore(delivery, paymentMeans);
}
}
}
/**
* Enhances party information with contact details from metadata
* @param doc XML document
* @param invoice Invoice data
*/
private enhancePartyInformationUBL(doc: Document, invoice: TInvoice): void {
// Enhance supplier party
this.addContactToPartyUBL(doc, 'cac:AccountingSupplierParty', (invoice.from as any)?.metadata?.contactInformation);
// Enhance customer party
this.addContactToPartyUBL(doc, 'cac:AccountingCustomerParty', (invoice.to as any)?.metadata?.contactInformation);
}
/**
* Adds contact information to a party in UBL document
* @param doc XML document
* @param partySelector Party selector
* @param contactInfo Contact information from metadata
*/
private addContactToPartyUBL(doc: Document, partySelector: string, contactInfo?: any): void {
if (!contactInfo) return;
const partyContainer = doc.getElementsByTagName(partySelector)[0];
if (!partyContainer) return;
const party = partyContainer.getElementsByTagName('cac:Party')[0];
if (!party) return;
// Check if Contact already exists
let contact = party.getElementsByTagName('cac:Contact')[0];
if (!contact && (contactInfo.name || contactInfo.phone || contactInfo.email)) {
contact = doc.createElement('cac:Contact');
// Insert after PartyName
const partyName = party.getElementsByTagName('cac:PartyName')[0];
if (partyName && partyName.parentNode) {
partyName.parentNode.insertBefore(contact, partyName.nextSibling);
} else {
party.appendChild(contact);
}
}
if (contact) {
// Add contact name
if (contactInfo.name && !contact.getElementsByTagName('cbc:Name')[0]) {
const name = doc.createElement('cbc:Name');
name.textContent = contactInfo.name;
contact.appendChild(name);
}
// Add telephone
if (contactInfo.phone && !contact.getElementsByTagName('cbc:Telephone')[0]) {
const phone = doc.createElement('cbc:Telephone');
phone.textContent = contactInfo.phone;
contact.appendChild(phone);
}
// Add email
if (contactInfo.email && !contact.getElementsByTagName('cbc:ElectronicMail')[0]) {
const email = doc.createElement('cbc:ElectronicMail');
email.textContent = contactInfo.email;
contact.appendChild(email);
}
}
}
/**
* Enhances line items with metadata in UBL document
* @param doc XML document
* @param invoice Invoice data
*/
private enhanceLineItemsUBL(doc: Document, invoice: TInvoice): void {
const invoiceLines = doc.getElementsByTagName('cac:InvoiceLine');
for (let i = 0; i < invoiceLines.length && i < invoice.items.length; i++) {
const line = invoiceLines[i];
const item = invoice.items[i];
const itemMetadata = (item as any).metadata;
if (!itemMetadata) continue;
const itemElement = line.getElementsByTagName('cac:Item')[0];
if (!itemElement) continue;
// Add item description
if (itemMetadata.description && !itemElement.getElementsByTagName('cbc:Description')[0]) {
const desc = doc.createElement('cbc:Description');
desc.textContent = itemMetadata.description;
// Insert before Name
const name = itemElement.getElementsByTagName('cbc:Name')[0];
if (name && name.parentNode) {
name.parentNode.insertBefore(desc, name);
} else {
itemElement.appendChild(desc);
}
}
// Add SellersItemIdentification
if (item.articleNumber && !itemElement.getElementsByTagName('cac:SellersItemIdentification')[0]) {
const sellerId = doc.createElement('cac:SellersItemIdentification');
const id = doc.createElement('cbc:ID');
id.textContent = item.articleNumber;
sellerId.appendChild(id);
itemElement.appendChild(sellerId);
}
// Add BuyersItemIdentification
if (itemMetadata.buyerItemID && !itemElement.getElementsByTagName('cac:BuyersItemIdentification')[0]) {
const buyerId = doc.createElement('cac:BuyersItemIdentification');
const id = doc.createElement('cbc:ID');
id.textContent = itemMetadata.buyerItemID;
buyerId.appendChild(id);
itemElement.appendChild(buyerId);
}
// Add StandardItemIdentification
if (itemMetadata.standardItemID && !itemElement.getElementsByTagName('cac:StandardItemIdentification')[0]) {
const standardId = doc.createElement('cac:StandardItemIdentification');
const id = doc.createElement('cbc:ID');
id.textContent = itemMetadata.standardItemID;
standardId.appendChild(id);
itemElement.appendChild(standardId);
}
// Add CommodityClassification
if (itemMetadata.commodityClassification && !itemElement.getElementsByTagName('cac:CommodityClassification')[0]) {
const classification = doc.createElement('cac:CommodityClassification');
const code = doc.createElement('cbc:ItemClassificationCode');
code.textContent = itemMetadata.commodityClassification;
classification.appendChild(code);
itemElement.appendChild(classification);
}
// Add additional item properties
if (itemMetadata.additionalProperties) {
for (const [propName, propValue] of Object.entries(itemMetadata.additionalProperties)) {
const additionalProp = doc.createElement('cac:AdditionalItemProperty');
const nameElement = doc.createElement('cbc:Name');
nameElement.textContent = propName;
additionalProp.appendChild(nameElement);
const valueElement = doc.createElement('cbc:Value');
valueElement.textContent = propValue as string;
additionalProp.appendChild(valueElement);
itemElement.appendChild(additionalProp);
}
}
}
}
}