From e5cb2ec4ea740294bd04c011fdfae687f0e21ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Olivi=C3=A9?= Date: Wed, 29 Jul 2026 11:42:24 +0000 Subject: [PATCH] fix: emit the mandatory NetworkID inside CardAccount The UBL 2.1 schema declares cbc:NetworkID as minOccurs="1" inside CardAccountType, so invoices carrying card payment instructions were rejected by XSD validation: cvc-complex-type.2.4.b: The content of element 'cac:CardAccount' is not complete. One of '{...:NetworkID}' is expected. The element has no EN 16931 business term, and GOBL's pay.Card has no counterpart to source it from, so we emit the "NA" placeholder that Peppol BIS documents for this syntax-only element. Co-Authored-By: Claude Opus 5 --- payment.go | 3 +++ payment_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/payment.go b/payment.go index 8975d0b..41e8ce6 100644 --- a/payment.go +++ b/payment.go @@ -78,6 +78,7 @@ type PrepaidPayment struct { } const sepaSchemeID = "SEPA" +const cardNetworkNotApplicable = "NA" func (ui *Invoice) addPayment(inv *bill.Invoice, ctx Context) error { if inv == nil || inv.Payment == nil { @@ -175,8 +176,10 @@ func (ui *Invoice) addPaymentInstructions(inv *bill.Invoice, ctx Context) error } } if instr.Card != nil { + network := cardNetworkNotApplicable ui.PaymentMeans[0].CardAccount = &CardAccount{ PrimaryAccountNumberID: &instr.Card.Last4, + NetworkID: &network, } if instr.Card.Holder != "" { ui.PaymentMeans[0].CardAccount.HolderName = &instr.Card.Holder diff --git a/payment_test.go b/payment_test.go index a5c8884..93ac726 100644 --- a/payment_test.go +++ b/payment_test.go @@ -71,6 +71,33 @@ func TestNewPayment(t *testing.T) { assert.Equal(t, "MANDATE-123", doc.PaymentMeans[0].PaymentMandate.ID.Value) }) + t.Run("card payment includes the network ID required by the schema", func(t *testing.T) { + env := loadTestEnvelope(t, "invoice-minimal.json") + + inv, ok := env.Extract().(*bill.Invoice) + require.True(t, ok) + + inv.Payment.Instructions.CreditTransfer = nil + inv.Payment.Instructions.Card = &pay.Card{Last4: "0312"} + + doc, err := ubl.ConvertInvoice(env) + require.NoError(t, err) + require.NotEmpty(t, doc.PaymentMeans) + + card := doc.PaymentMeans[0].CardAccount + require.NotNil(t, card) + require.NotNil(t, card.PrimaryAccountNumberID) + assert.Equal(t, "0312", *card.PrimaryAccountNumberID) + // cbc:NetworkID is mandatory in UBL, but has no EN 16931 business term. + require.NotNil(t, card.NetworkID) + assert.Equal(t, "NA", *card.NetworkID) + assert.Nil(t, card.HolderName) + + data, err := ubl.Bytes(doc) + require.NoError(t, err) + assert.Contains(t, string(data), "\n 0312\n NA\n ") + }) + t.Run("document type extension", func(t *testing.T) { env := loadTestEnvelope(t, "invoice-minimal.json")