From dcda59681baa7696a12b596826b25708d405f52b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:30:20 +0000 Subject: [PATCH 1/7] Initial plan From 38e2fd526cf9382874954b58250e3ebe21a3d585 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:43:54 +0000 Subject: [PATCH 2/7] Initial analysis - no code changes yet Agent-Logs-Url: https://github.com/PixelDrive/peppol-toolkit/sessions/66585be7-f643-4ca2-bff8-823420d86af2 Co-authored-by: SimonLoir <17649337+SimonLoir@users.noreply.github.com> --- package-lock.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 25c5387..0788729 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1136,7 +1136,6 @@ "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1186,7 +1185,6 @@ "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", @@ -1557,7 +1555,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2000,7 +1997,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -2055,7 +2051,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3147,7 +3142,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3725,7 +3719,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3888,7 +3881,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3958,7 +3950,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", From 7c2012393c6d39cc0fc6e2788c4d694f20c6f979 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:19:28 +0000 Subject: [PATCH 3/7] Update schemas to follow PEPPOL BIS Invoice 3 specification - Fix InvoicePeriodSchema: make descriptionCode optional (0..1) - Create Delivery schema (cac:Delivery 0..1) - Create AllowanceCharge schemas (document-level and line-level) - Create AdditionalDocumentReference schema - Update Invoice: add orderReference, billingReference, document references, delivery, allowanceCharge, projectReference - Update CreditNote: inherit new Invoice fields, remove projectReference - Update Line: add commodityClassification, lineAllowanceCharge, baseQuantity, priceAllowanceCharge, update standardItemIdentification and additionalItemProperties to match spec structure - Update PaymentMeans: add paymentDueDate - Update DocumentBuilder: handle all new fields - Update DocumentParser: handle all new fields - Update data files and tests for schema changes Agent-Logs-Url: https://github.com/PixelDrive/peppol-toolkit/sessions/66585be7-f643-4ca2-bff8-823420d86af2 Co-authored-by: SimonLoir <17649337+SimonLoir@users.noreply.github.com> --- src/builder/DocumentBuilder.ts | 673 ++++++++++++++++-- src/data/basic-creditNote.ts | 21 +- src/data/basic-invoice.ts | 9 +- .../common/AdditionalDocumentReference.ts | 29 + src/documents/common/AllowanceCharge.ts | 47 ++ src/documents/common/Delivery.ts | 15 + src/documents/common/InvoicePeriodSchema.ts | 2 +- src/documents/common/Line.ts | 39 +- src/documents/common/PaymentMeans.ts | 2 + src/documents/common/index.ts | 3 + src/documents/invoices/CreditNote.ts | 13 +- src/documents/invoices/Invoice.ts | 28 + src/parser/DocumentParser.ts | 481 +++++++++++-- src/parser/parserOptions.ts | 5 + tests/lines-creditnote.test.ts | 30 +- tests/lines-invoice.test.ts | 33 +- tests/parser-creditnote.test.ts | 4 +- 17 files changed, 1264 insertions(+), 170 deletions(-) create mode 100644 src/documents/common/AdditionalDocumentReference.ts create mode 100644 src/documents/common/AllowanceCharge.ts create mode 100644 src/documents/common/Delivery.ts diff --git a/src/builder/DocumentBuilder.ts b/src/builder/DocumentBuilder.ts index 1adb631..edeb304 100644 --- a/src/builder/DocumentBuilder.ts +++ b/src/builder/DocumentBuilder.ts @@ -66,12 +66,121 @@ export class DocumentBuilder { ? getDateString(invoice.dueDate) : undefined, 'cbc:InvoiceTypeCode': invoice.invoiceTypeCode, + ...(invoice.note + ? { 'cbc:Note': invoice.note } + : {}), + ...(invoice.taxPointDate + ? { + 'cbc:TaxPointDate': getDateString( + invoice.taxPointDate + ), + } + : {}), 'cbc:DocumentCurrencyCode': invoice.documentCurrencyCode, + ...(invoice.taxCurrencyCode + ? { + 'cbc:TaxCurrencyCode': invoice.taxCurrencyCode, + } + : {}), + ...(invoice.accountingCost + ? { 'cbc:AccountingCost': invoice.accountingCost } + : {}), 'cbc:BuyerReference': invoice.buyerReference, + ...(invoice.invoicePeriod + ? { + 'cac:InvoicePeriod': + this.__buildInvoicePeriod(invoice.invoicePeriod), + } + : {}), + ...(invoice.orderReference + ? { + 'cac:OrderReference': { + 'cbc:ID': invoice.orderReference.id, + ...(invoice.orderReference.salesOrderId + ? { + 'cbc:SalesOrderID': + invoice.orderReference + .salesOrderId, + } + : {}), + }, + } + : {}), + ...(invoice.billingReference && + invoice.billingReference.length > 0 + ? { + 'cac:BillingReference': + invoice.billingReference.map((ref) => ({ + 'cac:InvoiceDocumentReference': { + 'cbc:ID': ref.invoiceDocReference.id, + ...(ref.invoiceDocReference.issueDate + ? { + 'cbc:IssueDate': + getDateString( + ref.invoiceDocReference + .issueDate + ), + } + : {}), + }, + })), + } + : {}), + ...(invoice.despatchDocumentReference + ? { + 'cac:DespatchDocumentReference': { + 'cbc:ID': invoice.despatchDocumentReference, + }, + } + : {}), + ...(invoice.receiptDocumentReference + ? { + 'cac:ReceiptDocumentReference': { + 'cbc:ID': invoice.receiptDocumentReference, + }, + } + : {}), + ...(invoice.originatorDocumentReference + ? { + 'cac:OriginatorDocumentReference': { + 'cbc:ID': invoice.originatorDocumentReference, + }, + } + : {}), + ...(invoice.contractDocumentReference + ? { + 'cac:ContractDocumentReference': { + 'cbc:ID': invoice.contractDocumentReference, + }, + } + : {}), + ...(invoice.additionalDocumentReference && + invoice.additionalDocumentReference.length > 0 + ? { + 'cac:AdditionalDocumentReference': + invoice.additionalDocumentReference.map( + this.__buildAdditionalDocumentReference + ), + } + : {}), + ...(invoice.projectReference + ? { + 'cac:ProjectReference': { + 'cbc:ID': invoice.projectReference, + }, + } + : {}), 'cac:AccountingSupplierParty': this.__buildParty( invoice.seller ), 'cac:AccountingCustomerParty': this.__buildParty(invoice.buyer), + ...(invoice.delivery + ? { + 'cac:Delivery': this.__buildDelivery( + invoice.delivery + ), + } + : {}), 'cac:PaymentMeans': this.__buildPaymentMeans( invoice.paymentMeans ), @@ -82,6 +191,18 @@ export class DocumentBuilder { }, } : {}), + ...(invoice.allowanceCharge && + invoice.allowanceCharge.length > 0 + ? { + 'cac:AllowanceCharge': + invoice.allowanceCharge.map((ac) => + this.__buildAllowanceCharge( + ac, + invoice.documentCurrencyCode + ) + ), + } + : {}), 'cac:TaxTotal': this.__buildTaxTotal(invoice.taxTotal), 'cac:LegalMonetaryTotal': this.__buildMonetaryTotal( invoice.legalMonetaryTotal @@ -117,30 +238,121 @@ export class DocumentBuilder { 'cbc:ProfileID': creditNote.profileID ?? DEFAULT_PROFILE_ID, 'cbc:ID': creditNote.ID || 'AUTOGENERATE', 'cbc:IssueDate': getDateString(creditNote.issueDate), + ...(creditNote.taxPointDate + ? { + 'cbc:TaxPointDate': getDateString( + creditNote.taxPointDate + ), + } + : {}), 'cbc:CreditNoteTypeCode': creditNote.creditNoteTypeCode ?? 381, + ...(creditNote.note + ? { 'cbc:Note': creditNote.note } + : {}), 'cbc:DocumentCurrencyCode': creditNote.documentCurrencyCode || 'EUR', + ...(creditNote.taxCurrencyCode + ? { + 'cbc:TaxCurrencyCode': creditNote.taxCurrencyCode, + } + : {}), + ...(creditNote.accountingCost + ? { 'cbc:AccountingCost': creditNote.accountingCost } + : {}), 'cbc:BuyerReference': creditNote.buyerReference, - 'cac:BillingReference': - creditNote.billingReference && - creditNote.billingReference?.invoiceDocReference - ? { - 'cac:InvoiceDocumentReference': { - 'cbc:ID': - creditNote.billingReference - .invoiceDocReference.id, - 'cbc:IssueDate': - creditNote.billingReference - .invoiceDocReference.issueDate ?? 0, - }, - } - : {}, + ...(creditNote.invoicePeriod + ? { + 'cac:InvoicePeriod': + this.__buildInvoicePeriod( + creditNote.invoicePeriod + ), + } + : {}), + ...(creditNote.orderReference + ? { + 'cac:OrderReference': { + 'cbc:ID': creditNote.orderReference.id, + ...(creditNote.orderReference.salesOrderId + ? { + 'cbc:SalesOrderID': + creditNote.orderReference + .salesOrderId, + } + : {}), + }, + } + : {}), + ...(creditNote.billingReference && + creditNote.billingReference.length > 0 + ? { + 'cac:BillingReference': + creditNote.billingReference.map((ref) => ({ + 'cac:InvoiceDocumentReference': { + 'cbc:ID': ref.invoiceDocReference.id, + ...(ref.invoiceDocReference.issueDate + ? { + 'cbc:IssueDate': + getDateString( + ref.invoiceDocReference + .issueDate + ), + } + : {}), + }, + })), + } + : {}), + ...(creditNote.despatchDocumentReference + ? { + 'cac:DespatchDocumentReference': { + 'cbc:ID': creditNote.despatchDocumentReference, + }, + } + : {}), + ...(creditNote.receiptDocumentReference + ? { + 'cac:ReceiptDocumentReference': { + 'cbc:ID': creditNote.receiptDocumentReference, + }, + } + : {}), + ...(creditNote.contractDocumentReference + ? { + 'cac:ContractDocumentReference': { + 'cbc:ID': creditNote.contractDocumentReference, + }, + } + : {}), + ...(creditNote.additionalDocumentReference && + creditNote.additionalDocumentReference.length > 0 + ? { + 'cac:AdditionalDocumentReference': + creditNote.additionalDocumentReference.map( + this.__buildAdditionalDocumentReference + ), + } + : {}), + ...(creditNote.originatorDocumentReference + ? { + 'cac:OriginatorDocumentReference': { + 'cbc:ID': + creditNote.originatorDocumentReference, + }, + } + : {}), 'cac:AccountingSupplierParty': this.__buildParty( creditNote.seller ), 'cac:AccountingCustomerParty': this.__buildParty( creditNote.buyer ), + ...(creditNote.delivery + ? { + 'cac:Delivery': this.__buildDelivery( + creditNote.delivery + ), + } + : {}), 'cac:PaymentMeans': this.__buildPaymentMeans( creditNote.paymentMeans ), @@ -151,6 +363,18 @@ export class DocumentBuilder { }, } : {}), + ...(creditNote.allowanceCharge && + creditNote.allowanceCharge.length > 0 + ? { + 'cac:AllowanceCharge': + creditNote.allowanceCharge.map((ac) => + this.__buildAllowanceCharge( + ac, + creditNote.documentCurrencyCode + ) + ), + } + : {}), 'cac:TaxTotal': this.__buildTaxTotal(creditNote.taxTotal), 'cac:LegalMonetaryTotal': this.__buildMonetaryTotal( creditNote.legalMonetaryTotal @@ -377,49 +601,228 @@ export class DocumentBuilder { }; } - private __buildInvoiceLine(line: Invoice['invoiceLines'][number]) { + private __buildInvoicePeriod( + period: NonNullable + ) { return { - 'cbc:ID': line.id, - ...(line.note ? { 'cbc:Note': line.note } : {}), - 'cbc:InvoicedQuantity': { - '#text': line.invoicedQuantity.toFixed(2), - ...XMLAttributes({ - unitCode: line.unitCode, - }), - }, - 'cbc:LineExtensionAmount': { - '#text': line.lineExtensionAmount.toFixed(2), - ...XMLAttributes({ - currencyID: line.currency, - }), + ...(period.startDate + ? { 'cbc:StartDate': getDateString(period.startDate) } + : {}), + ...(period.endDate + ? { 'cbc:EndDate': getDateString(period.endDate) } + : {}), + ...(period.descriptionCode + ? { 'cbc:DescriptionCode': period.descriptionCode } + : {}), + }; + } + + private __buildDelivery(delivery: NonNullable) { + return { + ...(delivery.actualDeliveryDate + ? { + 'cbc:ActualDeliveryDate': getDateString( + delivery.actualDeliveryDate + ), + } + : {}), + ...(delivery.deliveryLocation + ? { + 'cac:DeliveryLocation': { + ...(delivery.deliveryLocation.id + ? { + 'cbc:ID': { + '#text': delivery.deliveryLocation.id, + ...(delivery.deliveryLocation + .locationSchemeID + ? XMLAttributes({ + schemeID: + delivery.deliveryLocation + .locationSchemeID, + }) + : {}), + }, + } + : {}), + ...(delivery.deliveryLocation.address + ? { + 'cac:Address': { + 'cbc:StreetName': + delivery.deliveryLocation.address + .streetName, + 'cbc:AdditionalStreetName': + delivery.deliveryLocation.address + .additionalStreetName, + 'cbc:CityName': + delivery.deliveryLocation.address + .cityName, + 'cbc:PostalZone': + delivery.deliveryLocation.address + .postalZone, + 'cac:Country': { + 'cbc:IdentificationCode': + delivery.deliveryLocation + .address.country, + }, + }, + } + : {}), + }, + } + : {}), + ...(delivery.deliveryPartyName + ? { + 'cac:DeliveryParty': { + 'cac:PartyName': { + 'cbc:Name': delivery.deliveryPartyName, + }, + }, + } + : {}), + }; + } + + private __buildAllowanceCharge( + ac: NonNullable[number], + documentCurrency: CurrencyCode + ) { + const currency = ac.currency ?? documentCurrency; + return { + 'cbc:ChargeIndicator': ac.chargeIndicator, + ...(ac.reasonCode + ? { + 'cbc:AllowanceChargeReasonCode': ac.reasonCode, + } + : {}), + ...(ac.reason + ? { 'cbc:AllowanceChargeReason': ac.reason } + : {}), + ...(ac.multiplierFactorNumeric !== undefined + ? { + 'cbc:MultiplierFactorNumeric': + ac.multiplierFactorNumeric, + } + : {}), + 'cbc:Amount': { + '#text': ac.amount.toFixed(2), + ...XMLAttributes({ currencyID: currency }), }, - 'cac:Item': { - 'cbc:Name': line.name, - 'cbc:Description': line.description, - 'cac:ClassifiedTaxCategory': { - 'cbc:ID': line.taxCategory.categoryCode, - 'cbc:Percent': line.taxCategory.percent?.toFixed(2), - 'cac:TaxScheme': { - 'cbc:ID': 'VAT', - }, + ...(ac.baseAmount !== undefined + ? { + 'cbc:BaseAmount': { + '#text': ac.baseAmount.toFixed(2), + ...XMLAttributes({ currencyID: currency }), + }, + } + : {}), + 'cac:TaxCategory': { + 'cbc:ID': ac.taxCategory.categoryCode, + ...(ac.taxCategory.percent !== undefined + ? { + 'cbc:Percent': ac.taxCategory.percent.toFixed(2), + } + : {}), + 'cac:TaxScheme': { + 'cbc:ID': 'VAT', }, }, - 'cac:Price': { - 'cbc:PriceAmount': { - '#text': line.price.toFixed(2), - ...XMLAttributes({ - currencyID: line.currency, - }), - }, + }; + } + + private __buildLineAllowanceCharge( + ac: NonNullable< + Invoice['invoiceLines'][number]['allowanceCharge'] + >[number], + lineCurrency: CurrencyCode + ) { + const currency = ac.currency ?? lineCurrency; + return { + 'cbc:ChargeIndicator': ac.chargeIndicator, + ...(ac.reasonCode + ? { + 'cbc:AllowanceChargeReasonCode': ac.reasonCode, + } + : {}), + ...(ac.reason + ? { 'cbc:AllowanceChargeReason': ac.reason } + : {}), + ...(ac.multiplierFactorNumeric !== undefined + ? { + 'cbc:MultiplierFactorNumeric': + ac.multiplierFactorNumeric, + } + : {}), + 'cbc:Amount': { + '#text': ac.amount.toFixed(2), + ...XMLAttributes({ currencyID: currency }), }, + ...(ac.baseAmount !== undefined + ? { + 'cbc:BaseAmount': { + '#text': ac.baseAmount.toFixed(2), + ...XMLAttributes({ currencyID: currency }), + }, + } + : {}), }; } - private _buildCreditNoteLine(line: CreditNote['creditNoteLines'][number]) { + private __buildAdditionalDocumentReference( + ref: NonNullable[number] + ) { + return { + 'cbc:ID': { + '#text': ref.id, + ...(ref.schemeID + ? XMLAttributes({ schemeID: ref.schemeID }) + : {}), + }, + ...(ref.documentTypeCode + ? { 'cbc:DocumentTypeCode': ref.documentTypeCode } + : {}), + ...(ref.documentDescription + ? { 'cbc:DocumentDescription': ref.documentDescription } + : {}), + ...(ref.attachment + ? { + 'cac:Attachment': { + ...(ref.attachment.embeddedDocumentBinaryObject + ? { + 'cbc:EmbeddedDocumentBinaryObject': { + '#text': + ref.attachment + .embeddedDocumentBinaryObject, + ...XMLAttributes({ + mimeCode: + ref.attachment.mimeCode ?? '', + filename: + ref.attachment.filename ?? '', + }), + }, + } + : {}), + ...(ref.attachment.externalReference + ? { + 'cac:ExternalReference': { + 'cbc:URI': + ref.attachment.externalReference, + }, + } + : {}), + }, + } + : {}), + }; + } + + private __buildLineItem( + line: Invoice['invoiceLines'][number], + quantityTag: string + ) { return { 'cbc:ID': line.id, ...(line.note ? { 'cbc:Note': line.note } : {}), - 'cbc:CreditedQuantity': { + [`cbc:${quantityTag}`]: { '#text': line.invoicedQuantity.toFixed(2), ...XMLAttributes({ unitCode: line.unitCode, @@ -431,9 +834,116 @@ export class DocumentBuilder { currencyID: line.currency, }), }, + ...(line.accountingCost + ? { 'cbc:AccountingCost': line.accountingCost } + : {}), + ...(line.invoicePeriod + ? { + 'cac:InvoicePeriod': { + ...(line.invoicePeriod.startDate + ? { + 'cbc:StartDate': getDateString( + line.invoicePeriod.startDate + ), + } + : {}), + ...(line.invoicePeriod.endDate + ? { + 'cbc:EndDate': getDateString( + line.invoicePeriod.endDate + ), + } + : {}), + }, + } + : {}), + ...(line.orderLineReference + ? { + 'cac:OrderLineReference': { + 'cbc:LineID': line.orderLineReference, + }, + } + : {}), + ...(line.documentReference + ? { + 'cac:DocumentReference': { + 'cbc:ID': line.documentReference, + 'cbc:DocumentTypeCode': '130', + }, + } + : {}), + ...(line.allowanceCharge && line.allowanceCharge.length > 0 + ? { + 'cac:AllowanceCharge': line.allowanceCharge.map( + (ac) => + this.__buildLineAllowanceCharge( + ac, + line.currency + ) + ), + } + : {}), 'cac:Item': { + ...(line.description + ? { 'cbc:Description': line.description } + : {}), 'cbc:Name': line.name, - 'cbc:Description': line.description, + ...(line.buyersItemIdentification + ? { + 'cac:BuyersItemIdentification': { + 'cbc:ID': line.buyersItemIdentification, + }, + } + : {}), + ...(line.sellersItemIdentification + ? { + 'cac:SellersItemIdentification': { + 'cbc:ID': line.sellersItemIdentification, + }, + } + : {}), + ...(line.standardItemIdentification + ? { + 'cac:StandardItemIdentification': { + 'cbc:ID': { + '#text': + line.standardItemIdentification.id, + ...XMLAttributes({ + schemeID: + line.standardItemIdentification + .schemeID, + }), + }, + }, + } + : {}), + ...(line.originCountry + ? { + 'cac:OriginCountry': { + 'cbc:IdentificationCode': line.originCountry, + }, + } + : {}), + ...(line.commodityClassification && + line.commodityClassification.length > 0 + ? { + 'cac:CommodityClassification': + line.commodityClassification.map((cc) => ({ + 'cbc:ItemClassificationCode': { + '#text': cc.code, + ...XMLAttributes({ + listID: cc.listID, + ...(cc.listVersionID + ? { + listVersionID: + cc.listVersionID, + } + : {}), + }), + }, + })), + } + : {}), 'cac:ClassifiedTaxCategory': { 'cbc:ID': line.taxCategory.categoryCode, 'cbc:Percent': line.taxCategory.percent?.toFixed(2), @@ -441,6 +951,16 @@ export class DocumentBuilder { 'cbc:ID': 'VAT', }, }, + ...(line.additionalItemProperties && + line.additionalItemProperties.length > 0 + ? { + 'cac:AdditionalItemProperty': + line.additionalItemProperties.map((prop) => ({ + 'cbc:Name': prop.name, + 'cbc:Value': prop.value, + })), + } + : {}), }, 'cac:Price': { 'cbc:PriceAmount': { @@ -449,7 +969,66 @@ export class DocumentBuilder { currencyID: line.currency, }), }, + ...(line.baseQuantity !== undefined + ? { + 'cbc:BaseQuantity': { + '#text': line.baseQuantity.toFixed(2), + ...XMLAttributes({ + unitCode: line.unitCode, + }), + }, + } + : {}), + ...(line.priceAllowanceCharge + ? { + 'cac:AllowanceCharge': { + 'cbc:ChargeIndicator': false, + 'cbc:Amount': { + '#text': + line.priceAllowanceCharge.amount.toFixed( + 2 + ), + ...XMLAttributes({ + currencyID: + line.priceAllowanceCharge.currency ?? + line.currency, + }), + }, + ...(line.priceAllowanceCharge.baseAmount !== + undefined + ? { + 'cbc:BaseAmount': { + '#text': + line.priceAllowanceCharge.baseAmount.toFixed( + 2 + ), + ...XMLAttributes({ + currencyID: + line.priceAllowanceCharge + .currency ?? + line.currency, + }), + }, + } + : {}), + }, + } + : {}), }, }; } + + private __buildInvoiceLine(line: Invoice['invoiceLines'][number]) { + return DocumentBuilder.prototype.__buildLineItem( + line, + 'InvoicedQuantity' + ); + } + + private _buildCreditNoteLine(line: CreditNote['creditNoteLines'][number]) { + return DocumentBuilder.prototype.__buildLineItem( + line, + 'CreditedQuantity' + ); + } } diff --git a/src/data/basic-creditNote.ts b/src/data/basic-creditNote.ts index fd25212..b97cbc7 100644 --- a/src/data/basic-creditNote.ts +++ b/src/data/basic-creditNote.ts @@ -6,12 +6,14 @@ export const basicCreditNote = { creditNoteTypeCode: 381, documentCurrencyCode: 'EUR', buyerReference: "Test Buyer's Reference", - billingReference: { - invoiceDocReference: { - id: 'INV-001', - issueDate: '2017-09-15', + billingReference: [ + { + invoiceDocReference: { + id: 'INV-001', + issueDate: '2017-09-15', + }, }, - }, + ], seller: { endPoint: { scheme: '9925', @@ -108,9 +110,12 @@ export const basicCreditNote = { price: 100, name: 'Petit poney', currency: 'EUR', - additionalItemProperties: { - test: 'hello world', - }, + additionalItemProperties: [ + { + name: 'test', + value: 'hello world', + }, + ], unitCode: 'H67', taxCategory: { percent: 21, diff --git a/src/data/basic-invoice.ts b/src/data/basic-invoice.ts index 12a1306..10736e7 100644 --- a/src/data/basic-invoice.ts +++ b/src/data/basic-invoice.ts @@ -103,9 +103,12 @@ export const basicInvoice = { price: 100, name: 'Petit poney', currency: 'EUR', - additionalItemProperties: { - test: 'hello world', - }, + additionalItemProperties: [ + { + name: 'test', + value: 'hello world', + }, + ], unitCode: 'H67', taxCategory: { percent: 21, diff --git a/src/documents/common/AdditionalDocumentReference.ts b/src/documents/common/AdditionalDocumentReference.ts new file mode 100644 index 0000000..02c7382 --- /dev/null +++ b/src/documents/common/AdditionalDocumentReference.ts @@ -0,0 +1,29 @@ +import { z } from 'zod'; + +/** + * Additional supporting document reference (BG-24) + * @see https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-AdditionalDocumentReference/ + */ +export const additionalDocumentReferenceSchema = z.object({ + /** Document identifier (BT-18, BT-122) */ + id: z.string().min(1), + /** Scheme identifier for the document ID */ + schemeID: z.string().optional(), + /** Code "130" for invoice object reference, "50" for project reference */ + documentTypeCode: z.string().optional(), + /** Description of the supporting document */ + documentDescription: z.string().optional(), + /** Embedded document attachment */ + attachment: z + .object({ + /** Base64 encoded embedded document */ + embeddedDocumentBinaryObject: z.string().optional(), + /** MIME type of the embedded document */ + mimeCode: z.string().optional(), + /** File name of the attached document */ + filename: z.string().optional(), + /** External document URL */ + externalReference: z.string().optional(), + }) + .optional(), +}); diff --git a/src/documents/common/AllowanceCharge.ts b/src/documents/common/AllowanceCharge.ts new file mode 100644 index 0000000..ff1e9a4 --- /dev/null +++ b/src/documents/common/AllowanceCharge.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; +import { CurrencyCodeSchema } from './CurrencyCodes'; +import { DutyTaxFeeCategoryCodeSchema } from './TaxCategory'; + +/** + * Document level allowance or charge (BG-20, BG-21) + * @see https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-AllowanceCharge/ + */ +export const allowanceChargeSchema = z.object({ + /** Use true for charges, false for allowances */ + chargeIndicator: z.boolean(), + reasonCode: z.string().optional(), + reason: z.string().optional(), + /** Percentage used to calculate the amount */ + multiplierFactorNumeric: z.number().min(0).optional(), + /** Amount without VAT */ + amount: z.number(), + /** Currency for amount fields – defaults to document currency when omitted */ + currency: CurrencyCodeSchema.optional(), + /** Base amount used with percentage to calculate the amount */ + baseAmount: z.number().optional(), + /** VAT category applied to this allowance/charge */ + taxCategory: z.object({ + categoryCode: DutyTaxFeeCategoryCodeSchema, + percent: z.number().min(0).max(100).optional(), + }), +}); + +/** + * Invoice line level allowance or charge (BG-27, BG-28) + * Same as document level but without tax category (tax is on the line item) + * @see https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-InvoiceLine/cac-AllowanceCharge/ + */ +export const lineAllowanceChargeSchema = z.object({ + /** Use true for charges, false for allowances */ + chargeIndicator: z.boolean(), + reasonCode: z.string().optional(), + reason: z.string().optional(), + /** Percentage used to calculate the amount */ + multiplierFactorNumeric: z.number().min(0).optional(), + /** Amount without VAT */ + amount: z.number(), + /** Currency for amount fields – defaults to document/line currency when omitted */ + currency: CurrencyCodeSchema.optional(), + /** Base amount used with percentage to calculate the amount */ + baseAmount: z.number().optional(), +}); diff --git a/src/documents/common/Delivery.ts b/src/documents/common/Delivery.ts new file mode 100644 index 0000000..44091c4 --- /dev/null +++ b/src/documents/common/Delivery.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; +import { date } from './Date'; +import { addressSchema } from './AdressSchema'; + +export const deliverySchema = z.object({ + actualDeliveryDate: date.optional(), + deliveryLocation: z + .object({ + id: z.string().min(1).optional(), + locationSchemeID: z.string().optional(), + address: addressSchema.optional(), + }) + .optional(), + deliveryPartyName: z.string().min(1).optional(), +}); diff --git a/src/documents/common/InvoicePeriodSchema.ts b/src/documents/common/InvoicePeriodSchema.ts index 6efa856..0266359 100644 --- a/src/documents/common/InvoicePeriodSchema.ts +++ b/src/documents/common/InvoicePeriodSchema.ts @@ -4,5 +4,5 @@ import { date } from './Date'; export const invoicePeriodSchema = z.object({ startDate: date.optional(), endDate: date.optional(), - descriptionCode: z.enum(['3', '35', '432']), + descriptionCode: z.enum(['3', '35', '432']).optional(), }); diff --git a/src/documents/common/Line.ts b/src/documents/common/Line.ts index 571e2cc..b5d14de 100644 --- a/src/documents/common/Line.ts +++ b/src/documents/common/Line.ts @@ -4,6 +4,7 @@ import { CountryCodeSchema } from './CountryCodes'; import { taxCategorySchema } from './TaxCategory'; import { CurrencyCodeSchema } from './CurrencyCodes'; import { UnitCodeSchema } from './UnitCodes'; +import { lineAllowanceChargeSchema } from './AllowanceCharge'; export const lineSchema = z.object({ id: z.string().min(1), @@ -19,21 +20,49 @@ export const lineSchema = z.object({ .optional(), orderLineReference: z.string().optional(), documentReference: z.string().optional(), - //TODO: add AllowanceCharge + allowanceCharge: z.array(lineAllowanceChargeSchema).optional(), name: z.string(), description: z.string().optional(), buyersItemIdentification: z.string().optional(), sellersItemIdentification: z.string().optional(), - standardItemIdentification: z.string().optional(), + standardItemIdentification: z + .object({ + id: z.string().min(1), + schemeID: z.string().min(1), + }) + .optional(), originCountry: CountryCodeSchema.optional(), - //TODO: add item classification + commodityClassification: z + .array( + z.object({ + code: z.string().min(1), + listID: z.string().min(1), + listVersionID: z.string().optional(), + }) + ) + .optional(), taxCategory: taxCategorySchema.pick({ categoryCode: true, percent: true, }), - additionalItemProperties: z.record(z.string(), z.any()).optional(), + additionalItemProperties: z + .array( + z.object({ + name: z.string().min(1), + value: z.string().min(1), + }) + ) + .optional(), price: z.number().min(0), currency: CurrencyCodeSchema, - //TODO: add price allowance and baseQT unitCode: UnitCodeSchema, + baseQuantity: z.number().min(0).optional(), + priceAllowanceCharge: z + .object({ + amount: z.number(), + /** Currency – defaults to line/document currency when omitted */ + currency: CurrencyCodeSchema.optional(), + baseAmount: z.number().optional(), + }) + .optional(), }); diff --git a/src/documents/common/PaymentMeans.ts b/src/documents/common/PaymentMeans.ts index 4f80beb..1a6c86a 100644 --- a/src/documents/common/PaymentMeans.ts +++ b/src/documents/common/PaymentMeans.ts @@ -1,10 +1,12 @@ import { z } from 'zod'; import { PaymentMeansCodeSchema } from './PaymentMeansCodes'; import { financialAccountSchema } from './FinancialAccount'; +import { date } from './Date'; export const paymentMeansSchema = z.object({ code: PaymentMeansCodeSchema, name: z.string().optional().describe('Payment means expressed as text'), + paymentDueDate: date.optional().describe('Payment due date (used in credit notes)'), paymentId: z.string().optional().describe('Used for reconciliation'), financialAccount: financialAccountSchema .optional() diff --git a/src/documents/common/index.ts b/src/documents/common/index.ts index 24f5e49..747dba8 100644 --- a/src/documents/common/index.ts +++ b/src/documents/common/index.ts @@ -12,3 +12,6 @@ export * from './TaxTotal'; export * from './Line'; export * from './InvoicePeriodSchema'; export * from './PaymentMeans'; +export * from './Delivery'; +export * from './AllowanceCharge'; +export * from './AdditionalDocumentReference'; diff --git a/src/documents/invoices/CreditNote.ts b/src/documents/invoices/CreditNote.ts index 51aaa90..8c7f994 100644 --- a/src/documents/invoices/CreditNote.ts +++ b/src/documents/invoices/CreditNote.ts @@ -1,16 +1,6 @@ import z from 'zod'; import { invoiceSchema } from './Invoice'; import { CreditNoteTypeCodeSchema } from './CreditNoteTypeCodes'; -import { date } from '../common'; - -const billingReference = z - .object({ - invoiceDocReference: z.object({ - id: z.string(), - issueDate: date.optional(), - }), - }) - .optional(); export const creditNoteSchema = invoiceSchema .extend({ @@ -18,8 +8,7 @@ export const creditNoteSchema = invoiceSchema profileID: z.string().optional(), creditNoteTypeCode: CreditNoteTypeCodeSchema, creditNoteLines: invoiceSchema.shape.invoiceLines.min(1), - billingReference: billingReference, }) - .omit({ invoiceTypeCode: true, invoiceLines: true, dueDate: true }); + .omit({ invoiceTypeCode: true, invoiceLines: true, dueDate: true, projectReference: true }); export type CreditNote = z.infer; diff --git a/src/documents/invoices/Invoice.ts b/src/documents/invoices/Invoice.ts index 8dc2821..68d1fb8 100644 --- a/src/documents/invoices/Invoice.ts +++ b/src/documents/invoices/Invoice.ts @@ -3,14 +3,24 @@ import { z } from 'zod'; import { CurrencyCodeSchema, date, + deliverySchema, invoicePeriodSchema, legalMonetaryTotalSchema, lineSchema, partySchema, paymentMeansSchema, taxTotalSchema, + allowanceChargeSchema, + additionalDocumentReferenceSchema, } from '../common'; +const billingReferenceSchema = z.object({ + invoiceDocReference: z.object({ + id: z.string(), + issueDate: date.optional(), + }), +}); + export const invoiceSchema = z.object({ customizationID: z.string().optional(), profileID: z.string().optional(), @@ -26,14 +36,32 @@ export const invoiceSchema = z.object({ buyerReference: z.string().optional(), invoicePeriod: invoicePeriodSchema.optional(), + orderReference: z + .object({ + id: z.string().min(1), + salesOrderId: z.string().optional(), + }) + .optional(), + billingReference: z.array(billingReferenceSchema).optional(), + despatchDocumentReference: z.string().optional(), + receiptDocumentReference: z.string().optional(), + originatorDocumentReference: z.string().optional(), + contractDocumentReference: z.string().optional(), + additionalDocumentReference: z + .array(additionalDocumentReferenceSchema) + .optional(), + projectReference: z.string().optional(), + seller: partySchema, buyer: partySchema, + delivery: deliverySchema.optional(), paymentMeans: paymentMeansSchema.array().optional(), paymentTermsNote: z .string() .optional() .describe('Payment terms that apply (including penalties)'), + allowanceCharge: z.array(allowanceChargeSchema).optional(), taxTotal: z.array(taxTotalSchema).min(1).max(2), legalMonetaryTotal: legalMonetaryTotalSchema, invoiceLines: z.array(lineSchema).min(1), diff --git a/src/parser/DocumentParser.ts b/src/parser/DocumentParser.ts index feade9c..0c5d7b2 100644 --- a/src/parser/DocumentParser.ts +++ b/src/parser/DocumentParser.ts @@ -31,22 +31,52 @@ export class DocumentParser { taxCurrencyCode: this.__str(inv['cbc:TaxCurrencyCode']), accountingCost: this.__str(inv['cbc:AccountingCost']), buyerReference: this.__str(inv['cbc:BuyerReference']), + invoicePeriod: this.__parseInvoicePeriod(inv['cac:InvoicePeriod']), + orderReference: this.__parseOrderReference( + inv['cac:OrderReference'] + ), + billingReference: this.__parseBillingReferences( + inv['cac:BillingReference'] + ), + despatchDocumentReference: this.__parseSimpleDocRef( + inv['cac:DespatchDocumentReference'] + ), + receiptDocumentReference: this.__parseSimpleDocRef( + inv['cac:ReceiptDocumentReference'] + ), + originatorDocumentReference: this.__parseSimpleDocRef( + inv['cac:OriginatorDocumentReference'] + ), + contractDocumentReference: this.__parseSimpleDocRef( + inv['cac:ContractDocumentReference'] + ), + additionalDocumentReference: + this.__parseAdditionalDocumentReferences( + inv['cac:AdditionalDocumentReference'] + ), + projectReference: this.__parseSimpleDocRef( + inv['cac:ProjectReference'] + ), seller: this.__parseParty( inv['cac:AccountingSupplierParty']?.['cac:Party'] ), buyer: this.__parseParty( inv['cac:AccountingCustomerParty']?.['cac:Party'] ), + delivery: this.__parseDelivery(inv['cac:Delivery']), paymentMeans: this.__parsePaymentMeans(inv['cac:PaymentMeans']), paymentTermsNote: inv['cac:PaymentTerms'] ? this.__str(inv['cac:PaymentTerms']['cbc:Note']) : undefined, + allowanceCharge: this.__parseAllowanceCharges( + inv['cac:AllowanceCharge'] + ), taxTotal: this.__parseTaxTotal(inv['cac:TaxTotal'] ?? []), legalMonetaryTotal: this.__parseMonetaryTotal( inv['cac:LegalMonetaryTotal'] ), invoiceLines: (inv['cac:InvoiceLine'] ?? []).map((line: unknown) => - this.__parseInvoiceLine(line) + this.__parseLineItem(line, 'InvoicedQuantity') ), }; @@ -63,9 +93,6 @@ export class DocumentParser { const cn = parsed['CreditNote']; if (!cn) throw new Error('Not a valid UBL CreditNote document'); - const billingRef = cn['cac:BillingReference']; - const invoiceDocRef = billingRef?.['cac:InvoiceDocumentReference']; - const raw = { customizationID: this.__str(cn['cbc:CustomizationID']), profileID: this.__str(cn['cbc:ProfileID']), @@ -78,32 +105,50 @@ export class DocumentParser { taxCurrencyCode: this.__str(cn['cbc:TaxCurrencyCode']), accountingCost: this.__str(cn['cbc:AccountingCost']), buyerReference: this.__str(cn['cbc:BuyerReference']), - billingReference: invoiceDocRef - ? { - invoiceDocReference: { - id: this.__str(invoiceDocRef['cbc:ID'])!, - issueDate: this.__str( - invoiceDocRef['cbc:IssueDate'] - ), - }, - } - : undefined, + invoicePeriod: this.__parseInvoicePeriod(cn['cac:InvoicePeriod']), + orderReference: this.__parseOrderReference( + cn['cac:OrderReference'] + ), + billingReference: this.__parseBillingReferences( + cn['cac:BillingReference'] + ), + despatchDocumentReference: this.__parseSimpleDocRef( + cn['cac:DespatchDocumentReference'] + ), + receiptDocumentReference: this.__parseSimpleDocRef( + cn['cac:ReceiptDocumentReference'] + ), + originatorDocumentReference: this.__parseSimpleDocRef( + cn['cac:OriginatorDocumentReference'] + ), + contractDocumentReference: this.__parseSimpleDocRef( + cn['cac:ContractDocumentReference'] + ), + additionalDocumentReference: + this.__parseAdditionalDocumentReferences( + cn['cac:AdditionalDocumentReference'] + ), seller: this.__parseParty( cn['cac:AccountingSupplierParty']?.['cac:Party'] ), buyer: this.__parseParty( cn['cac:AccountingCustomerParty']?.['cac:Party'] ), + delivery: this.__parseDelivery(cn['cac:Delivery']), paymentMeans: this.__parsePaymentMeans(cn['cac:PaymentMeans']), paymentTermsNote: cn['cac:PaymentTerms'] ? this.__str(cn['cac:PaymentTerms']['cbc:Note']) : undefined, + allowanceCharge: this.__parseAllowanceCharges( + cn['cac:AllowanceCharge'] + ), taxTotal: this.__parseTaxTotal(cn['cac:TaxTotal'] ?? []), legalMonetaryTotal: this.__parseMonetaryTotal( cn['cac:LegalMonetaryTotal'] ), creditNoteLines: (cn['cac:CreditNoteLine'] ?? []).map( - (line: unknown) => this.__parseCreditNoteLine(line) + (line: unknown) => + this.__parseLineItem(line, 'CreditedQuantity') ), }; @@ -186,6 +231,208 @@ export class DocumentParser { }; } + private __parseInvoicePeriod( + period: Record | undefined + ): Invoice['invoicePeriod'] { + if (!period) return undefined; + return { + startDate: this.__str(period['cbc:StartDate']), + endDate: this.__str(period['cbc:EndDate']), + descriptionCode: this.__str( + period['cbc:DescriptionCode'] + ) as Invoice['invoicePeriod'] extends infer P + ? P extends { descriptionCode?: infer D } + ? D + : never + : never, + }; + } + + private __parseOrderReference( + ref: Record | undefined + ): Invoice['orderReference'] { + if (!ref) return undefined; + return { + id: this.__str(ref['cbc:ID'])!, + salesOrderId: this.__str(ref['cbc:SalesOrderID']), + }; + } + + private __parseBillingReferences( + refs: unknown[] | undefined + ): Invoice['billingReference'] { + if (!refs || refs.length === 0) return undefined; + return refs.map((ref) => { + const r = ref as Record; + const docRef = r['cac:InvoiceDocumentReference'] as Record< + string, + unknown + >; + return { + invoiceDocReference: { + id: this.__str(docRef?.['cbc:ID'])!, + issueDate: this.__str(docRef?.['cbc:IssueDate']), + }, + }; + }); + } + + private __parseSimpleDocRef( + ref: Record | undefined + ): string | undefined { + if (!ref) return undefined; + return this.__str(ref['cbc:ID']); + } + + private __parseAdditionalDocumentReferences( + refs: unknown[] | undefined + ): Invoice['additionalDocumentReference'] { + if (!refs || refs.length === 0) return undefined; + return refs.map((ref) => { + const r = ref as Record; + const idEl = r['cbc:ID']; + const attachment = r['cac:Attachment'] as + | Record + | undefined; + const binaryObj = attachment?.['cbc:EmbeddedDocumentBinaryObject']; + const extRef = attachment?.['cac:ExternalReference'] as + | Record + | undefined; + return { + id: this.__text(idEl)!, + schemeID: this.__attr(idEl, 'schemeID'), + documentTypeCode: this.__str(r['cbc:DocumentTypeCode']), + documentDescription: this.__str(r['cbc:DocumentDescription']), + attachment: attachment + ? { + embeddedDocumentBinaryObject: + this.__text(binaryObj), + mimeCode: this.__attr(binaryObj, 'mimeCode'), + filename: this.__attr(binaryObj, 'filename'), + externalReference: extRef + ? this.__str(extRef['cbc:URI']) + : undefined, + } + : undefined, + }; + }); + } + + private __parseDelivery( + delivery: Record | undefined + ): Invoice['delivery'] { + if (!delivery) return undefined; + const location = delivery['cac:DeliveryLocation'] as + | Record + | undefined; + const locationAddress = location?.['cac:Address'] as + | Record + | undefined; + const locationCountry = locationAddress?.['cac:Country'] as + | Record + | undefined; + const deliveryParty = delivery['cac:DeliveryParty'] as + | Record + | undefined; + const partyName = deliveryParty?.['cac:PartyName'] as + | Record + | undefined; + + return { + actualDeliveryDate: this.__str( + delivery['cbc:ActualDeliveryDate'] + ), + deliveryLocation: location + ? { + id: this.__text(location['cbc:ID']), + locationSchemeID: this.__attr( + location['cbc:ID'], + 'schemeID' + ), + address: locationAddress + ? { + streetName: this.__str( + locationAddress['cbc:StreetName'] + ), + additionalStreetName: this.__str( + locationAddress[ + 'cbc:AdditionalStreetName' + ] + ), + cityName: this.__str( + locationAddress['cbc:CityName'] + ), + postalZone: this.__str( + locationAddress['cbc:PostalZone'] + ), + countrySubentity: this.__str( + locationAddress['cbc:CountrySubentity'] + ), + country: this.__str( + locationCountry?.[ + 'cbc:IdentificationCode' + ] + ) as z.infer< + typeof partySchema + >['address']['country'], + } + : undefined, + } + : undefined, + deliveryPartyName: partyName + ? this.__str(partyName['cbc:Name']) + : undefined, + }; + } + + private __parseAllowanceCharges( + charges: unknown[] | undefined + ): Invoice['allowanceCharge'] { + if (!charges || charges.length === 0) return undefined; + return charges.map((c) => { + const ac = c as Record; + const amtEl = ac['cbc:Amount']; + const baseAmtEl = ac['cbc:BaseAmount']; + const taxCat = (ac['cac:TaxCategory'] ?? {}) as Record< + string, + unknown + >; + return { + chargeIndicator: ac['cbc:ChargeIndicator'] === 'true' || ac['cbc:ChargeIndicator'] === true, + reasonCode: this.__str( + ac['cbc:AllowanceChargeReasonCode'] + ), + reason: this.__str(ac['cbc:AllowanceChargeReason']), + multiplierFactorNumeric: + ac['cbc:MultiplierFactorNumeric'] !== undefined + ? Number(ac['cbc:MultiplierFactorNumeric']) + : undefined, + amount: Number(this.__text(amtEl)), + currency: this.__attr( + amtEl, + 'currencyID' + ) as Invoice['allowanceCharge'] extends (infer U)[] + ? U extends { currency?: infer C } + ? C + : never + : never, + baseAmount: + baseAmtEl !== undefined + ? Number(this.__text(baseAmtEl)) + : undefined, + taxCategory: { + categoryCode: this.__str( + taxCat['cbc:ID'] + ) as Invoice['taxTotal'][number]['subTotals'][number]['taxCategory']['categoryCode'], + percent: + taxCat['cbc:Percent'] !== undefined + ? Number(taxCat['cbc:Percent']) + : undefined, + }, + }; + }); + } + private __parsePaymentMeans( means: unknown[] | undefined ): Invoice['paymentMeans'] { @@ -206,6 +453,8 @@ export class DocumentParser { ? C : never : never, + name: this.__attr(pm['cbc:PaymentMeansCode'], 'name'), + paymentDueDate: this.__str(pm['cbc:PaymentDueDate']), paymentId: this.__str(pm['cbc:PaymentID']), financialAccount: fa ? { @@ -321,18 +570,31 @@ export class DocumentParser { }; } - private __parseInvoiceLine( - line: unknown + private __parseLineItem( + line: unknown, + quantityTag: string ): Invoice['invoiceLines'][number] { const l = line as Record; - const qtyEl = l['cbc:InvoicedQuantity']; + const qtyEl = l[`cbc:${quantityTag}`]; const amtEl = l['cbc:LineExtensionAmount']; const item = (l['cac:Item'] ?? {}) as Record; - const priceEl = ( - l['cac:Price'] as Record | undefined - )?.['cbc:PriceAmount']; + const priceSection = (l['cac:Price'] ?? {}) as Record< + string, + unknown + >; + const priceEl = priceSection['cbc:PriceAmount']; + const baseQtyEl = priceSection['cbc:BaseQuantity']; + const priceAC = priceSection['cac:AllowanceCharge'] as + | Record + | undefined; const taxCat = (item['cac:ClassifiedTaxCategory'] ?? {}) as Record; + const stdIdEl = (item['cac:StandardItemIdentification'] as Record | undefined)?.['cbc:ID']; + const commodityClassifications = (item['cac:CommodityClassification'] as unknown[] | undefined) ?? []; + const additionalProperties = (item['cac:AdditionalItemProperty'] as unknown[] | undefined) ?? []; + + // Parse line allowance charges + const lineACs = l['cac:AllowanceCharge'] as unknown[] | undefined; return { id: this.__str(l['cbc:ID'])!, @@ -347,8 +609,115 @@ export class DocumentParser { amtEl, 'currencyID' ) as Invoice['invoiceLines'][number]['currency'], + accountingCost: this.__str(l['cbc:AccountingCost']), + invoicePeriod: l['cac:InvoicePeriod'] + ? { + startDate: this.__str( + (l['cac:InvoicePeriod'] as Record)[ + 'cbc:StartDate' + ] + ), + endDate: this.__str( + (l['cac:InvoicePeriod'] as Record)[ + 'cbc:EndDate' + ] + ), + } + : undefined, + orderLineReference: l['cac:OrderLineReference'] + ? this.__str( + ( + l['cac:OrderLineReference'] as Record< + string, + unknown + > + )['cbc:LineID'] + ) + : undefined, + documentReference: l['cac:DocumentReference'] + ? this.__str( + ( + l['cac:DocumentReference'] as Record< + string, + unknown + > + )['cbc:ID'] + ) + : undefined, + allowanceCharge: + lineACs && lineACs.length > 0 + ? lineACs.map((ac) => { + const a = ac as Record; + const acAmtEl = a['cbc:Amount']; + const acBaseEl = a['cbc:BaseAmount']; + return { + chargeIndicator: + a['cbc:ChargeIndicator'] === 'true' || a['cbc:ChargeIndicator'] === true, + reasonCode: this.__str( + a['cbc:AllowanceChargeReasonCode'] + ), + reason: this.__str( + a['cbc:AllowanceChargeReason'] + ), + multiplierFactorNumeric: + a['cbc:MultiplierFactorNumeric'] !== undefined + ? Number( + a['cbc:MultiplierFactorNumeric'] + ) + : undefined, + amount: Number(this.__text(acAmtEl)), + currency: this.__attr(acAmtEl, 'currencyID'), + baseAmount: + acBaseEl !== undefined + ? Number(this.__text(acBaseEl)) + : undefined, + }; + }) + : undefined, name: this.__str(item['cbc:Name'])!, description: this.__str(item['cbc:Description']), + buyersItemIdentification: this.__str( + ( + item['cac:BuyersItemIdentification'] as + | Record + | undefined + )?.['cbc:ID'] + ), + sellersItemIdentification: this.__str( + ( + item['cac:SellersItemIdentification'] as + | Record + | undefined + )?.['cbc:ID'] + ), + standardItemIdentification: stdIdEl + ? { + id: this.__text(stdIdEl)!, + schemeID: this.__attr(stdIdEl, 'schemeID')!, + } + : undefined, + originCountry: this.__str( + ( + item['cac:OriginCountry'] as + | Record + | undefined + )?.['cbc:IdentificationCode'] + ) as Invoice['invoiceLines'][number]['originCountry'], + commodityClassification: + commodityClassifications.length > 0 + ? commodityClassifications.map((cc) => { + const c = cc as Record; + const codeEl = c['cbc:ItemClassificationCode']; + return { + code: this.__text(codeEl)!, + listID: this.__attr(codeEl, 'listID')!, + listVersionID: this.__attr( + codeEl, + 'listVersionID' + ), + }; + }) + : undefined, price: Number(this.__text(priceEl)), taxCategory: { categoryCode: this.__str( @@ -359,47 +728,35 @@ export class DocumentParser { ? Number(taxCat['cbc:Percent']) : undefined, }, - }; - } - - private __parseCreditNoteLine( - line: unknown - ): CreditNote['creditNoteLines'][number] { - const l = line as Record; - const qtyEl = l['cbc:CreditedQuantity']; - const amtEl = l['cbc:LineExtensionAmount']; - const item = (l['cac:Item'] ?? {}) as Record; - const priceEl = ( - l['cac:Price'] as Record | undefined - )?.['cbc:PriceAmount']; - const taxCat = (item['cac:ClassifiedTaxCategory'] ?? - {}) as Record; - - return { - id: this.__str(l['cbc:ID'])!, - note: this.__str(l['cbc:Note']), - invoicedQuantity: Number(this.__text(qtyEl)), - unitCode: this.__attr( - qtyEl, - 'unitCode' - ) as CreditNote['creditNoteLines'][number]['unitCode'], - lineExtensionAmount: Number(this.__text(amtEl)), - currency: this.__attr( - amtEl, - 'currencyID' - ) as CreditNote['creditNoteLines'][number]['currency'], - name: this.__str(item['cbc:Name'])!, - description: this.__str(item['cbc:Description']), - price: Number(this.__text(priceEl)), - taxCategory: { - categoryCode: this.__str( - taxCat['cbc:ID'] - ) as CreditNote['creditNoteLines'][number]['taxCategory']['categoryCode'], - percent: - taxCat['cbc:Percent'] !== undefined - ? Number(taxCat['cbc:Percent']) - : undefined, - }, + additionalItemProperties: + additionalProperties.length > 0 + ? additionalProperties.map((prop) => { + const p = prop as Record; + return { + name: this.__str(p['cbc:Name'])!, + value: this.__str(p['cbc:Value'])!, + }; + }) + : undefined, + baseQuantity: + baseQtyEl !== undefined + ? Number(this.__text(baseQtyEl)) + : undefined, + priceAllowanceCharge: priceAC + ? { + amount: Number(this.__text(priceAC['cbc:Amount'])), + currency: this.__attr( + priceAC['cbc:Amount'], + 'currencyID' + ) as Invoice['invoiceLines'][number]['currency'], + baseAmount: + priceAC['cbc:BaseAmount'] !== undefined + ? Number( + this.__text(priceAC['cbc:BaseAmount']) + ) + : undefined, + } + : undefined, }; } diff --git a/src/parser/parserOptions.ts b/src/parser/parserOptions.ts index 2894a75..579cce3 100644 --- a/src/parser/parserOptions.ts +++ b/src/parser/parserOptions.ts @@ -12,6 +12,11 @@ export const parserOptions = { 'cac:TaxTotal', 'cac:TaxSubtotal', 'cac:PartyIdentification', + 'cac:BillingReference', + 'cac:AllowanceCharge', + 'cac:AdditionalDocumentReference', + 'cac:CommodityClassification', + 'cac:AdditionalItemProperty', ].includes(tagName); }, } satisfies X2jOptions; diff --git a/tests/lines-creditnote.test.ts b/tests/lines-creditnote.test.ts index c9a1973..5744848 100644 --- a/tests/lines-creditnote.test.ts +++ b/tests/lines-creditnote.test.ts @@ -41,9 +41,9 @@ describe('CreditNote Lines', () => { price: 100, name: 'Petit poney', currency: 'EUR', - additionalItemProperties: { - test: 'hello world', - }, + additionalItemProperties: [ + { name: 'test', value: 'hello world' }, + ], unitCode: 'DAY', taxCategory: { percent: 21, @@ -62,9 +62,9 @@ describe('CreditNote Lines', () => { price: 100, name: 'Petit poney', currency: 'EUR', - additionalItemProperties: { - test: 'hello world', - }, + additionalItemProperties: [ + { name: 'test', value: 'hello world' }, + ], unitCode: 'DAY', taxCategory: { percent: 21, @@ -86,9 +86,9 @@ describe('CreditNote Lines', () => { price: 100, name: 'Petit poney', currency: 'INV', - additionalItemProperties: { - test: 'hello world', - }, + additionalItemProperties: [ + { name: 'test', value: 'hello world' }, + ], unitCode: 'DAY', taxCategory: { percent: 21, @@ -108,9 +108,9 @@ describe('CreditNote Lines', () => { price: 100, name: 'Petit poney', currency: 'EUR', - additionalItemProperties: { - test: 'hello world', - }, + additionalItemProperties: [ + { name: 'test', value: 'hello world' }, + ], unitCode: 'DAY1', taxCategory: { percent: 21, @@ -130,9 +130,9 @@ describe('CreditNote Lines', () => { price: 100, name: 'Petit poney', currency: 'EUR', - additionalItemProperties: { - test: 'hello world', - }, + additionalItemProperties: [ + { name: 'test', value: 'hello world' }, + ], unitCode: 'H67', taxCategory: { percent: 21, diff --git a/tests/lines-invoice.test.ts b/tests/lines-invoice.test.ts index 20661c8..0641603 100644 --- a/tests/lines-invoice.test.ts +++ b/tests/lines-invoice.test.ts @@ -41,9 +41,12 @@ describe('Invoice Lines', () => { price: 100, name: 'Petit poney', currency: 'EUR', - additionalItemProperties: { - test: 'hello world', - }, + additionalItemProperties: [ + { + name: 'test', + value: 'hello world', + }, + ], unitCode: 'DAY', taxCategory: { percent: 21, @@ -62,9 +65,9 @@ describe('Invoice Lines', () => { price: 100, name: 'Petit poney', currency: 'EUR', - additionalItemProperties: { - test: 'hello world', - }, + additionalItemProperties: [ + { name: 'test', value: 'hello world' }, + ], unitCode: 'DAY', taxCategory: { percent: 21, @@ -86,9 +89,9 @@ describe('Invoice Lines', () => { price: 100, name: 'Petit poney', currency: 'INV', - additionalItemProperties: { - test: 'hello world', - }, + additionalItemProperties: [ + { name: 'test', value: 'hello world' }, + ], unitCode: 'DAY', taxCategory: { percent: 21, @@ -108,9 +111,9 @@ describe('Invoice Lines', () => { price: 100, name: 'Petit poney', currency: 'EUR', - additionalItemProperties: { - test: 'hello world', - }, + additionalItemProperties: [ + { name: 'test', value: 'hello world' }, + ], unitCode: 'DAY1', taxCategory: { percent: 21, @@ -130,9 +133,9 @@ describe('Invoice Lines', () => { price: 100, name: 'Petit poney', currency: 'EUR', - additionalItemProperties: { - test: 'hello world', - }, + additionalItemProperties: [ + { name: 'test', value: 'hello world' }, + ], unitCode: 'H67', taxCategory: { percent: 21, diff --git a/tests/parser-creditnote.test.ts b/tests/parser-creditnote.test.ts index 3e0a7b6..0b6a545 100644 --- a/tests/parser-creditnote.test.ts +++ b/tests/parser-creditnote.test.ts @@ -91,8 +91,8 @@ describe('CreditNote Parser', () => { it('should preserve billing reference', () => { const xml = toolkit.creditNoteToPeppolUBL(basicCreditNote); const parsed = toolkit.peppolUBLToCreditNote(xml); - expect(parsed.billingReference?.invoiceDocReference.id).toBe('INV-001'); - expect(parsed.billingReference?.invoiceDocReference.issueDate).toBe( + expect(parsed.billingReference?.[0].invoiceDocReference.id).toBe('INV-001'); + expect(parsed.billingReference?.[0].invoiceDocReference.issueDate).toBe( '2017-09-15' ); }); From d6b4525647bcd4a90e5140693e8eb6aafb5af6cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:23:15 +0000 Subject: [PATCH 4/7] Add __bool helper for ChargeIndicator parsing (code review feedback) Agent-Logs-Url: https://github.com/PixelDrive/peppol-toolkit/sessions/66585be7-f643-4ca2-bff8-823420d86af2 Co-authored-by: SimonLoir <17649337+SimonLoir@users.noreply.github.com> --- src/parser/DocumentParser.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/parser/DocumentParser.ts b/src/parser/DocumentParser.ts index 0c5d7b2..cf4aa39 100644 --- a/src/parser/DocumentParser.ts +++ b/src/parser/DocumentParser.ts @@ -796,4 +796,11 @@ export class DocumentParser { } return undefined; } + + /** + * Parses a boolean value from XML (handles both string "true"/"false" and native booleans). + */ + private __bool(value: unknown): boolean { + return value === 'true' || value === true; + } } From bd8a5d147c611a77ae9a75114b998a70a9db192e Mon Sep 17 00:00:00 2001 From: Simon Loir Date: Wed, 1 Apr 2026 16:40:57 +0200 Subject: [PATCH 5/7] refactor: reorganize imports and improve code formatting in DocumentParser --- src/parser/DocumentParser.ts | 119 ++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 58 deletions(-) diff --git a/src/parser/DocumentParser.ts b/src/parser/DocumentParser.ts index cf4aa39..06945ab 100644 --- a/src/parser/DocumentParser.ts +++ b/src/parser/DocumentParser.ts @@ -1,9 +1,14 @@ import { XMLParser } from 'fast-xml-parser'; import { parserOptions } from './parserOptions'; -import { Invoice, invoiceSchema } from '../documents/invoices/Invoice'; -import { CreditNote, creditNoteSchema } from '../documents/invoices/CreditNote'; +import { + CreditNote, + creditNoteSchema, + CurrencyCodeSchema, + Invoice, + invoiceSchema, + partySchema, +} from '../documents'; import { z } from 'zod'; -import { partySchema } from '../documents/common/Parties'; export class DocumentParser { private __parser = new XMLParser(parserOptions); @@ -155,7 +160,9 @@ export class DocumentParser { return creditNoteSchema.parse(raw); } - private __parseParty(party: Record): z.infer { + private __parseParty( + party: Record + ): z.infer { if (!party) throw new Error('Missing party in UBL document'); const endpointEl = party['cbc:EndpointID']; @@ -166,7 +173,10 @@ export class DocumentParser { return { id: this.__text(idEl)!, scheme: this.__attr(idEl, 'schemeID'), - } as { id: string; scheme?: z.infer['endPoint']['scheme'] }; + } as { + id: string; + scheme?: z.infer['endPoint']['scheme']; + }; }); const postal = @@ -195,9 +205,7 @@ export class DocumentParser { }, identification: identifications.length > 0 ? identifications : undefined, - name: partyNameEl - ? this.__str(partyNameEl['cbc:Name']) - : undefined, + name: partyNameEl ? this.__str(partyNameEl['cbc:Name']) : undefined, address: { streetName: this.__str(postal['cbc:StreetName']), additionalStreetName: this.__str( @@ -305,8 +313,7 @@ export class DocumentParser { documentDescription: this.__str(r['cbc:DocumentDescription']), attachment: attachment ? { - embeddedDocumentBinaryObject: - this.__text(binaryObj), + embeddedDocumentBinaryObject: this.__text(binaryObj), mimeCode: this.__attr(binaryObj, 'mimeCode'), filename: this.__attr(binaryObj, 'filename'), externalReference: extRef @@ -339,9 +346,7 @@ export class DocumentParser { | undefined; return { - actualDeliveryDate: this.__str( - delivery['cbc:ActualDeliveryDate'] - ), + actualDeliveryDate: this.__str(delivery['cbc:ActualDeliveryDate']), deliveryLocation: location ? { id: this.__text(location['cbc:ID']), @@ -355,9 +360,7 @@ export class DocumentParser { locationAddress['cbc:StreetName'] ), additionalStreetName: this.__str( - locationAddress[ - 'cbc:AdditionalStreetName' - ] + locationAddress['cbc:AdditionalStreetName'] ), cityName: this.__str( locationAddress['cbc:CityName'] @@ -369,9 +372,7 @@ export class DocumentParser { locationAddress['cbc:CountrySubentity'] ), country: this.__str( - locationCountry?.[ - 'cbc:IdentificationCode' - ] + locationCountry?.['cbc:IdentificationCode'] ) as z.infer< typeof partySchema >['address']['country'], @@ -398,10 +399,10 @@ export class DocumentParser { unknown >; return { - chargeIndicator: ac['cbc:ChargeIndicator'] === 'true' || ac['cbc:ChargeIndicator'] === true, - reasonCode: this.__str( - ac['cbc:AllowanceChargeReasonCode'] - ), + chargeIndicator: + ac['cbc:ChargeIndicator'] === 'true' || + ac['cbc:ChargeIndicator'] === true, + reasonCode: this.__str(ac['cbc:AllowanceChargeReasonCode']), reason: this.__str(ac['cbc:AllowanceChargeReason']), multiplierFactorNumeric: ac['cbc:MultiplierFactorNumeric'] !== undefined @@ -485,8 +486,10 @@ export class DocumentParser { const sub = st as Record; const taxableAmtEl = sub['cbc:TaxableAmount']; const taxAmtEl = sub['cbc:TaxAmount']; - const cat = (sub['cac:TaxCategory'] ?? - {}) as Record; + const cat = (sub['cac:TaxCategory'] ?? {}) as Record< + string, + unknown + >; return { taxableAmount: Number(this.__text(taxableAmtEl)), taxableAmountCurrency: this.__attr( @@ -532,7 +535,10 @@ export class DocumentParser { ): Invoice['legalMonetaryTotal']['currency'] | undefined => { const el = total[`cbc:${name}`]; return el !== undefined - ? (this.__attr(el, 'currencyID') as Invoice['legalMonetaryTotal']['currency']) + ? (this.__attr( + el, + 'currencyID' + ) as Invoice['legalMonetaryTotal']['currency']) : undefined; }; @@ -564,9 +570,7 @@ export class DocumentParser { prepaidAmount: get('PrepaidAmount'), prepaidAmountCurrency: getCurrency('PrepaidAmount'), payableRoundingAmount: get('PayableRoundingAmount'), - payableRoundingAmountCurrency: getCurrency( - 'PayableRoundingAmount' - ), + payableRoundingAmountCurrency: getCurrency('PayableRoundingAmount'), }; } @@ -578,20 +582,26 @@ export class DocumentParser { const qtyEl = l[`cbc:${quantityTag}`]; const amtEl = l['cbc:LineExtensionAmount']; const item = (l['cac:Item'] ?? {}) as Record; - const priceSection = (l['cac:Price'] ?? {}) as Record< - string, - unknown - >; + const priceSection = (l['cac:Price'] ?? {}) as Record; const priceEl = priceSection['cbc:PriceAmount']; const baseQtyEl = priceSection['cbc:BaseQuantity']; const priceAC = priceSection['cac:AllowanceCharge'] as | Record | undefined; - const taxCat = (item['cac:ClassifiedTaxCategory'] ?? - {}) as Record; - const stdIdEl = (item['cac:StandardItemIdentification'] as Record | undefined)?.['cbc:ID']; - const commodityClassifications = (item['cac:CommodityClassification'] as unknown[] | undefined) ?? []; - const additionalProperties = (item['cac:AdditionalItemProperty'] as unknown[] | undefined) ?? []; + const taxCat = (item['cac:ClassifiedTaxCategory'] ?? {}) as Record< + string, + unknown + >; + const stdIdEl = ( + item['cac:StandardItemIdentification'] as + | Record + | undefined + )?.['cbc:ID']; + const commodityClassifications = + (item['cac:CommodityClassification'] as unknown[] | undefined) ?? + []; + const additionalProperties = + (item['cac:AdditionalItemProperty'] as unknown[] | undefined) ?? []; // Parse line allowance charges const lineACs = l['cac:AllowanceCharge'] as unknown[] | undefined; @@ -626,22 +636,16 @@ export class DocumentParser { : undefined, orderLineReference: l['cac:OrderLineReference'] ? this.__str( - ( - l['cac:OrderLineReference'] as Record< - string, - unknown - > - )['cbc:LineID'] + (l['cac:OrderLineReference'] as Record)[ + 'cbc:LineID' + ] ) : undefined, documentReference: l['cac:DocumentReference'] ? this.__str( - ( - l['cac:DocumentReference'] as Record< - string, - unknown - > - )['cbc:ID'] + (l['cac:DocumentReference'] as Record)[ + 'cbc:ID' + ] ) : undefined, allowanceCharge: @@ -652,7 +656,8 @@ export class DocumentParser { const acBaseEl = a['cbc:BaseAmount']; return { chargeIndicator: - a['cbc:ChargeIndicator'] === 'true' || a['cbc:ChargeIndicator'] === true, + a['cbc:ChargeIndicator'] === 'true' || + a['cbc:ChargeIndicator'] === true, reasonCode: this.__str( a['cbc:AllowanceChargeReasonCode'] ), @@ -661,12 +666,12 @@ export class DocumentParser { ), multiplierFactorNumeric: a['cbc:MultiplierFactorNumeric'] !== undefined - ? Number( - a['cbc:MultiplierFactorNumeric'] - ) + ? Number(a['cbc:MultiplierFactorNumeric']) : undefined, amount: Number(this.__text(acAmtEl)), - currency: this.__attr(acAmtEl, 'currencyID'), + currency: CurrencyCodeSchema.parse( + this.__attr(acAmtEl, 'currencyID') + ), baseAmount: acBaseEl !== undefined ? Number(this.__text(acBaseEl)) @@ -751,9 +756,7 @@ export class DocumentParser { ) as Invoice['invoiceLines'][number]['currency'], baseAmount: priceAC['cbc:BaseAmount'] !== undefined - ? Number( - this.__text(priceAC['cbc:BaseAmount']) - ) + ? Number(this.__text(priceAC['cbc:BaseAmount'])) : undefined, } : undefined, From 093527bc6f11407f02c5ae8ac54573d6a861e034 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:55:50 +0000 Subject: [PATCH 6/7] Fix: wire __bool helper in parser, fix this-binding bug in builder line methods - Replace inline boolean expressions with __bool() helper in parser - Fix __buildInvoiceLine/_buildCreditNoteLine losing `this` context when passed as .map() callbacks and using DocumentBuilder.prototype - Call __buildLineItem directly with arrow functions to preserve `this` - Remove now-unused wrapper methods Agent-Logs-Url: https://github.com/PixelDrive/peppol-toolkit/sessions/b68d20b9-7ae6-4674-b8a6-f82e74b3eb83 Co-authored-by: SimonLoir <17649337+SimonLoir@users.noreply.github.com> --- src/builder/DocumentBuilder.ts | 21 ++++----------------- src/parser/DocumentParser.ts | 10 ++++------ 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/src/builder/DocumentBuilder.ts b/src/builder/DocumentBuilder.ts index edeb304..090f7f3 100644 --- a/src/builder/DocumentBuilder.ts +++ b/src/builder/DocumentBuilder.ts @@ -207,8 +207,8 @@ export class DocumentBuilder { 'cac:LegalMonetaryTotal': this.__buildMonetaryTotal( invoice.legalMonetaryTotal ), - 'cac:InvoiceLine': invoice.invoiceLines.map( - this.__buildInvoiceLine + 'cac:InvoiceLine': invoice.invoiceLines.map((line) => + this.__buildLineItem(line, 'InvoicedQuantity') ), }, }; @@ -380,7 +380,8 @@ export class DocumentBuilder { creditNote.legalMonetaryTotal ), 'cac:CreditNoteLine': creditNote.creditNoteLines.map( - this._buildCreditNoteLine + (line) => + this.__buildLineItem(line, 'CreditedQuantity') ), }, }; @@ -1017,18 +1018,4 @@ export class DocumentBuilder { }, }; } - - private __buildInvoiceLine(line: Invoice['invoiceLines'][number]) { - return DocumentBuilder.prototype.__buildLineItem( - line, - 'InvoicedQuantity' - ); - } - - private _buildCreditNoteLine(line: CreditNote['creditNoteLines'][number]) { - return DocumentBuilder.prototype.__buildLineItem( - line, - 'CreditedQuantity' - ); - } } diff --git a/src/parser/DocumentParser.ts b/src/parser/DocumentParser.ts index 06945ab..9b5607c 100644 --- a/src/parser/DocumentParser.ts +++ b/src/parser/DocumentParser.ts @@ -399,9 +399,7 @@ export class DocumentParser { unknown >; return { - chargeIndicator: - ac['cbc:ChargeIndicator'] === 'true' || - ac['cbc:ChargeIndicator'] === true, + chargeIndicator: this.__bool(ac['cbc:ChargeIndicator']), reasonCode: this.__str(ac['cbc:AllowanceChargeReasonCode']), reason: this.__str(ac['cbc:AllowanceChargeReason']), multiplierFactorNumeric: @@ -655,9 +653,9 @@ export class DocumentParser { const acAmtEl = a['cbc:Amount']; const acBaseEl = a['cbc:BaseAmount']; return { - chargeIndicator: - a['cbc:ChargeIndicator'] === 'true' || - a['cbc:ChargeIndicator'] === true, + chargeIndicator: this.__bool( + a['cbc:ChargeIndicator'] + ), reasonCode: this.__str( a['cbc:AllowanceChargeReasonCode'] ), From 16bfe44d7839ae742605e7fbb232a284c2f71ba7 Mon Sep 17 00:00:00 2001 From: Simon Loir Date: Wed, 1 Apr 2026 18:23:56 +0200 Subject: [PATCH 7/7] chore: bump version to 0.7.0 and update keywords in package.json --- package.json | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 09ef55c..0abe54a 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,18 @@ { "name": "@pixeldrive/peppol-toolkit", - "version": "0.6.0", + "version": "0.7.0", "description": "A TypeScript toolkit for building and reading peppol UBL documents", "keywords": [ - "typescript", "peppol", - "pixeldrive" + "e-invoicing", + "peppol-toolkit", + "pixeldrive", + "UBL", + "BIS", + "XML", + "UBL-Invoice", + "UBL-CreditNote", + "parser" ], "license": "MIT", "homepage": "",