ZUGFeRD / mustangproject

Open Source Java e-Invoicing library, validator and tool (Factur-X/ZUGFeRD, UNCEFACT/CII XRechnung)
http://www.mustangproject.org
Apache License 2.0
234 stars 132 forks source link

Invalid XML for SEPA direct debit, if DirectDebitMandateID is provided #565

Open pinhead84 opened 1 day ago

pinhead84 commented 1 day ago

ZUGFeRD2PullProvider generates invalid XML for payments via SEPA direct debit, if a DirectDebitMandateID is provided.

The XML elements in <ram:SpecifiedTradePaymentTerms> are in the wrong order.

2024-11-21_08-03

This leads to this error message during validation:

Invalid content was found starting with element '{"urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100":DueDateDateTime}'. One of '{"urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100":DirectDebitMandateID, "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100":PartialPaymentPercent, "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100":PaymentMeansID, "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100":PartialPaymentAmount, "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100":ApplicableTradePaymentPenaltyTerms, "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100":ApplicableTradePaymentDiscountTerms, "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100":PayeeTradeParty}' is expected.

According to the XML schema, the <ram:DueDateDateTime/> element must be placed before the <ram:DirectDebitMandateID/> element:

<xsd:complexType name="TradePaymentTermsType">
  <xsd:sequence>
    <xsd:element name="ID" type="qdt:PaymentTermsIDType" minOccurs="0"/>
    <xsd:element name="FromEventCode" type="qdt:PaymentTermsEventTimeReferenceCodeType" minOccurs="0"/>
    <xsd:element name="SettlementPeriodMeasure" type="udt:MeasureType" minOccurs="0"/>
    <xsd:element name="Description" type="udt:TextType" minOccurs="0" maxOccurs="unbounded"/>
    <xsd:element name="DueDateDateTime" type="udt:DateTimeType" minOccurs="0"/>
    <xsd:element name="TypeCode" type="qdt:PaymentTermsTypeCodeType" minOccurs="0"/>
    <xsd:element name="InstructionTypeCode" type="udt:CodeType" minOccurs="0"/>
    <xsd:element name="DirectDebitMandateID" type="udt:IDType" minOccurs="0" maxOccurs="unbounded"/>
    <xsd:element name="PartialPaymentPercent" type="udt:PercentType" minOccurs="0"/>
    <xsd:element name="PaymentMeansID" type="udt:IDType" minOccurs="0" maxOccurs="unbounded"/>
    <xsd:element name="PartialPaymentAmount" type="udt:AmountType" minOccurs="0" maxOccurs="unbounded"/>
    <xsd:element name="ApplicableTradePaymentPenaltyTerms" type="ram:TradePaymentPenaltyTermsType" minOccurs="0"/>
    <xsd:element name="ApplicableTradePaymentDiscountTerms" type="ram:TradePaymentDiscountTermsType" minOccurs="0"/>
    <xsd:element name="PayeeTradeParty" type="ram:TradePartyType" minOccurs="0" maxOccurs="unbounded"/>
    </xsd:sequence>
</xsd:complexType>

This is a quick and hacky workaround, via a custom ZUGFeRD2PullProvider and ZUGFeRDExporterFromPDFA. It's written in Kotlin but can be easyly adopted to Java.

class CustomZUGFeRDExporterFromPDFA : ZUGFeRDExporterFromPDFA() {
    override fun determineAndSetExporter(pdfAVersion: Int) {
        super.determineAndSetExporter(pdfAVersion)
        if (theExporter == null) {
            return
        }

        if (theExporter is ZUGFeRDExporterFromA3) {
            theExporter = object : ZUGFeRDExporterFromA3() {
                init {
                    setXMLProvider(CustomZUGFeRD2PullProvider())
                }
            }
            return
        }

        if (theExporter is ZUGFeRDExporterFromA1) {
            theExporter = object : ZUGFeRDExporterFromA1() {
                init {
                    setXMLProvider(CustomZUGFeRD2PullProvider())
                }
            }
            return
        }
    }
}

class CustomZUGFeRD2PullProvider : ZUGFeRD2PullProvider() {
    companion object {
        val elementOrderForSpecifiedTradePaymentTerms = listOf(
            "ID",
            "FromEventCode",
            "SettlementPeriodMeasure",
            "Description",
            "DueDateDateTime",
            "TypeCode",
            "InstructionTypeCode",
            "DirectDebitMandateID",
            "PartialPaymentPercent",
            "PaymentMeansID",
            "PartialPaymentAmount",
            "ApplicableTradePaymentPenaltyTerms",
            "ApplicableTradePaymentDiscountTerms",
            "PayeeTradeParty",
        )
    }

    override fun generateXML(trans: IExportableTransaction?) {
        super.generateXML(trans)

        val doc = DocumentHelper.parseText(
            zugferdData.toString(Charsets.UTF_8)
        )

        @Suppress("SpellCheckingInspection")
        val namespaceMap = mapOf(
            "ram" to "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
        )

        val xpath = doc.createXPath("//ram:SpecifiedTradePaymentTerms")
        xpath.setNamespaceURIs(namespaceMap)
        xpath.selectNodes(doc)
            .forEach { node -> fixSpecifiedTradePaymentTerms(node as Element) }

        zugferdData = XMLTools.removeBOM(
            doc.asXML().toByteArray(charset = Charsets.UTF_8)
        )
    }

    private fun fixSpecifiedTradePaymentTerms(element: Element) {
        val childElements = buildList {
            element.elements().forEach { child ->
                element.remove(child)
                add(child)
            }
        }

        elementOrderForSpecifiedTradePaymentTerms.forEach { childName ->
            childElements
                .filter { it.name == childName }
                .forEach { child ->
                    element.add(child)
                }
        }
    }
}