diff --git a/library/src/main/java/org/mustangproject/Invoice.java b/library/src/main/java/org/mustangproject/Invoice.java index ee836d0c..881864c7 100644 --- a/library/src/main/java/org/mustangproject/Invoice.java +++ b/library/src/main/java/org/mustangproject/Invoice.java @@ -76,7 +76,7 @@ public class Invoice implements IExportableTransaction { protected String creditorReferenceID; // required when direct debit is used. private BigDecimal roundingAmount=null; private String paymentReference; // Remittance information / Verwendungszweck, BT-83 - + private String businessProcessId; public Invoice() { ZFItems = new ArrayList<>(); cashDiscounts = new ArrayList<>(); @@ -1167,4 +1167,14 @@ public class Invoice implements IExportableTransaction { return this; } + public Invoice setBusinessProcessId(String id) { + this.businessProcessId = id; + return this; + } + + @Override + public String getBusinessProcessId() { + return businessProcessId; + } + } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java b/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java index 4e4166a5..f3e87aab 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java @@ -630,4 +630,16 @@ public interface IExportableTransaction { default String getCreditorReferenceID() { return null; } + + /** + * BT-23 Business process identifier + * /rsm:CrossIndustryInvoice/rsm:ExchangedDocumentContext/ + * ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID + * + * @return business process ID (e.g. "B1" or a URN) or null if not provided + */ + default String getBusinessProcessId() { + return null; + } + } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java index bb9bd831..fbc32abc 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java @@ -380,7 +380,12 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { xml += "" + trans.getTestIndicator() + ""; } - if (getProfile() == Profiles.getByName("XRechnung")) { + String businessProcessId = trans.getBusinessProcessId(); + if (businessProcessId != null && !businessProcessId.isBlank()) { + xml += "\n" + + "" + XMLTools.encodeXML(businessProcessId) + "\n" + + "\n"; + }else if (getProfile() == Profiles.getByName("XRechnung")) { xml += "\n" + "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0\n" + "\n"; diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/BusinessProcessIdCiiTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/BusinessProcessIdCiiTest.java new file mode 100644 index 00000000..8d7f9b9a --- /dev/null +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/BusinessProcessIdCiiTest.java @@ -0,0 +1,121 @@ +package org.mustangproject.ZUGFeRD; + +import org.junit.jupiter.api.Test; +import org.mustangproject.ZUGFeRD.Profiles; +import org.mustangproject.ZUGFeRD.ZUGFeRD2PullProvider; + +import org.mustangproject.Invoice; +import org.mustangproject.Item; +import org.mustangproject.Product; +import org.mustangproject.TradeParty; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for BT-23 (BusinessProcessSpecifiedDocumentContextParameter/ID) emission in CII output. + * + * Assumptions / prerequisites for the proposed fix: + * - IExportableTransaction#getBusinessProcessId() exists (as a default method returning null) + * - Invoice#setBusinessProcessId(String) exists and Invoice overrides getBusinessProcessId() + * - ZUGFeRD2PullProvider writes BT-23 if a custom businessProcessId is provided, + * otherwise keeps the current XRechnung default behavior. + */ +public class BusinessProcessIdCiiTest { + + @Test + void xrechnung_nonRegression_defaultBusinessProcessId_isStillEmitted() throws Exception { + // ===== GIVEN: A minimal invoice (as provided) WITHOUT setting businessProcessId ===== + + TradeParty buyer = new TradeParty("Client X", "3 rue C", "33000", "Bordeaux", "FR"); + TradeParty seller = new TradeParty("Mairie A", "1 rue A", "75001", "Paris", "FR"); + + Invoice invoice = new Invoice() + .setNumber("INV-CALC-001") // BT-1: Invoice number -> /.../ExchangedDocument/ram:ID + .setIssueDate(new Date()) // BT-2: Issue date + .setDeliveryDate(new Date()) // Delivery date (depending on profile/exporter mapping) + .setDueDate(new Date()) // BT-9: Payment due date + .setCurrency("EUR") // BT-5: Invoice currency code + .setSender(buyer) + .setRecipient(seller); + + Product service = new Product("Prestation", "Service intercommunal", "C62", new BigDecimal("20.00")); + invoice.addItem(new Item(service, BigDecimal.ONE, BigDecimal.TEN).setTax(BigDecimal.valueOf(20))); + + // ===== WHEN: Generating CII XML using the XRechnung profile ===== + + ZUGFeRD2PullProvider provider = new ZUGFeRD2PullProvider(); + provider.setProfile(Profiles.getByName("XRechnung")); + provider.generateXML(invoice); + + String xml = new String(provider.getXML(), StandardCharsets.UTF_8); + + // ===== THEN: Non-regression check ===== + // XRechnung currently emits BT-23 with a default fixed URN. + // This behavior must remain unchanged when businessProcessId is NOT explicitly provided by the user. + + assertTrue( + xml.contains(""), + "XRechnung should still emit BusinessProcessSpecifiedDocumentContextParameter (BT-23)." + ); + + assertTrue( + xml.contains("urn:fdc:peppol.eu:2017:poacc:billing:01:1.0"), + "XRechnung default BT-23 URN must remain unchanged when businessProcessId is not set." + ); + } + + @Test + void whenBusinessProcessIdIsSet_itIsEmitted_inExtendedProfile() throws Exception { + // ===== GIVEN: The same minimal invoice, but WITH an explicit businessProcessId ===== + + TradeParty buyer = new TradeParty("Client X", "3 rue C", "33000", "Bordeaux", "FR"); + TradeParty seller = new TradeParty("Mairie A", "1 rue A", "75001", "Paris", "FR"); + + Invoice invoice = new Invoice() + .setNumber("INV-CALC-001") + .setIssueDate(new Date()) + .setDeliveryDate(new Date()) + .setDueDate(new Date()) + .setCurrency("EUR") + .setSender(buyer) + .setRecipient(seller); + + Product service = new Product("Prestation", "Service intercommunal", "C62", new BigDecimal("20.00")); + invoice.addItem(new Item(service, BigDecimal.ONE, BigDecimal.TEN).setTax(BigDecimal.valueOf(20))); + + // Proposed new API: user-provided BT-23 value + invoice.setBusinessProcessId("B1"); + // BT-23 path: + // /rsm:CrossIndustryInvoice/rsm:ExchangedDocumentContext/ + // ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID + + // ===== WHEN: Generating CII XML using the EXTENDED profile ===== + + ZUGFeRD2PullProvider provider = new ZUGFeRD2PullProvider(); + provider.setProfile(Profiles.getByName("EXTENDED")); + provider.generateXML(invoice); + + String xml = new String(provider.getXML(), StandardCharsets.UTF_8); + + // ===== THEN: BT-23 must be emitted with the user-provided value ===== + + assertTrue( + xml.contains(""), + "When businessProcessId is set, BT-23 must be emitted even for EXTENDED." + ); + + assertTrue( + xml.contains("B1"), + "BT-23 must contain the user-provided businessProcessId." + ); + + // Extra sanity check: the guideline/specification identifier (BT-24) should still be present + assertTrue( + xml.contains(""), + "BT-24 (GuidelineSpecifiedDocumentContextParameter) must be present." + ); + } +}