diff --git a/payment.go b/payment.go index 0e12c080..f8153012 100644 --- a/payment.go +++ b/payment.go @@ -131,28 +131,32 @@ func (ui *Invoice) addPaymentInstructions(instr *pay.Instructions) error { ui.PaymentMeans[0].PayeeFinancialAccount = newCreditTransferAccount(instr.CreditTransfer[0]) } if instr.DirectDebit != nil { - ui.PaymentMeans[0].PaymentMandate = &PaymentMandate{ - ID: IDType{Value: instr.DirectDebit.Ref}, + mandate := &PaymentMandate{} + if instr.DirectDebit.Ref != "" { + mandate.ID = IDType{Value: instr.DirectDebit.Ref} } if instr.DirectDebit.Account != "" { - ui.PaymentMeans[0].PayerFinancialAccount = &FinancialAccount{ + mandate.PayerFinancialAccount = &FinancialAccount{ ID: &instr.DirectDebit.Account, } } + ui.PaymentMeans[0].PaymentMandate = mandate } if instr.Card != nil { - ui.PaymentMeans[0].CardAccount = &CardAccount{ - PrimaryAccountNumberID: &instr.Card.Last4, + card := &CardAccount{} + if instr.Card.Last4 != "" { + card.PrimaryAccountNumberID = &instr.Card.Last4 } if instr.Card.Holder != "" { - ui.PaymentMeans[0].CardAccount.HolderName = &instr.Card.Holder + card.HolderName = &instr.Card.Holder } + ui.PaymentMeans[0].CardAccount = card } return nil } func newCreditTransferAccount(ct *pay.CreditTransfer) *FinancialAccount { - pfa := new(FinancialAccount) + pfa := &FinancialAccount{} if ct.IBAN != "" { pfa.ID = &ct.IBAN } else if ct.Number != "" { @@ -164,6 +168,9 @@ func newCreditTransferAccount(ct *pay.CreditTransfer) *FinancialAccount { if ct.BIC != "" { pfa.FinancialInstitutionBranch = &Branch{ID: &ct.BIC} } + if pfa.ID == nil && pfa.Name == nil && pfa.FinancialInstitutionBranch == nil { + return nil + } return pfa } @@ -193,7 +200,7 @@ func (ui *Invoice) addPaymentTerms(inv *bill.Invoice, pymt *bill.PaymentDetails) } } else if len(pymt.Terms.DueDates) == 1 && ui.CreditNoteTypeCode == "" { ui.DueDate = formatDate(*pymt.Terms.DueDates[0].Date) - } else { + } else if pymt.Terms.Notes != "" { ui.PaymentTerms = append(ui.PaymentTerms, PaymentTerms{ Note: []string{pymt.Terms.Notes}, }) diff --git a/payment_test.go b/payment_test.go index f498b351..e2408bbf 100644 --- a/payment_test.go +++ b/payment_test.go @@ -5,6 +5,7 @@ import ( ubl "github.com/invopop/gobl.ubl" "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/pay" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -36,6 +37,99 @@ func TestNewPayment(t *testing.T) { assert.Equal(t, "DNBANOKK", *doc.PaymentMeans[0].PayeeFinancialAccount.FinancialInstitutionBranch.ID) }) + t.Run("credit transfer with no account fields omits financial account", func(t *testing.T) { + env, err := loadTestEnvelope("invoice-minimal.json") + require.NoError(t, err) + + inv, ok := env.Extract().(*bill.Invoice) + require.True(t, ok) + + inv.Payment.Instructions.CreditTransfer = []*pay.CreditTransfer{{}} + + doc, err := ubl.ConvertInvoice(env) + require.NoError(t, err) + assert.Nil(t, doc.PaymentMeans[0].PayeeFinancialAccount) + }) + + t.Run("card with empty last4 omits PAN", func(t *testing.T) { + env, err := loadTestEnvelope("invoice-minimal.json") + require.NoError(t, err) + + inv, ok := env.Extract().(*bill.Invoice) + require.True(t, ok) + + inv.Payment.Instructions.Card = &pay.Card{Holder: "John Doe"} + + doc, err := ubl.ConvertInvoice(env) + require.NoError(t, err) + require.NotNil(t, doc.PaymentMeans[0].CardAccount) + assert.Nil(t, doc.PaymentMeans[0].CardAccount.PrimaryAccountNumberID) + assert.Equal(t, "John Doe", *doc.PaymentMeans[0].CardAccount.HolderName) + }) + + t.Run("direct debit with empty ref omits mandate ID", func(t *testing.T) { + env, err := loadTestEnvelope("invoice-minimal.json") + require.NoError(t, err) + + inv, ok := env.Extract().(*bill.Invoice) + require.True(t, ok) + + inv.Payment.Instructions.DirectDebit = &pay.DirectDebit{Account: "DE89370400440532013000"} + + doc, err := ubl.ConvertInvoice(env) + require.NoError(t, err) + require.NotNil(t, doc.PaymentMeans[0].PaymentMandate) + assert.Empty(t, doc.PaymentMeans[0].PaymentMandate.ID.Value) + assert.Equal(t, "DE89370400440532013000", *doc.PaymentMeans[0].PaymentMandate.PayerFinancialAccount.ID) + }) + + t.Run("instruction detail mapped to payment means name", func(t *testing.T) { + env, err := loadTestEnvelope("invoice-minimal.json") + require.NoError(t, err) + + inv, ok := env.Extract().(*bill.Invoice) + require.True(t, ok) + + inv.Payment.Instructions.Detail = "Bank transfer" + + doc, err := ubl.ConvertInvoice(env) + require.NoError(t, err) + require.NotNil(t, doc.PaymentMeans[0].PaymentMeansCode.Name) + assert.Equal(t, "Bank transfer", *doc.PaymentMeans[0].PaymentMeansCode.Name) + }) + + t.Run("payment terms with empty notes omits payment terms", func(t *testing.T) { + env, err := loadTestEnvelope("invoice-minimal.json") + require.NoError(t, err) + + inv, ok := env.Extract().(*bill.Invoice) + require.True(t, ok) + + inv.Payment.Terms.Notes = "" + + doc, err := ubl.ConvertInvoice(env) + require.NoError(t, err) + assert.Empty(t, doc.PaymentTerms) + }) + + t.Run("BT-90 creditor ID on supplier when no payee", func(t *testing.T) { + env, err := loadTestEnvelope("invoice-minimal.json") + require.NoError(t, err) + + inv, ok := env.Extract().(*bill.Invoice) + require.True(t, ok) + + inv.Payment.Instructions.DirectDebit = &pay.DirectDebit{Creditor: "DE98ZZZ09999999999"} + + doc, err := ubl.ConvertInvoice(env) + require.NoError(t, err) + require.Nil(t, doc.PayeeParty) + ids := doc.AccountingSupplierParty.Party.PartyIdentification + require.NotEmpty(t, ids) + assert.Equal(t, "DE98ZZZ09999999999", ids[len(ids)-1].ID.Value) + assert.Equal(t, "SEPA", *ids[len(ids)-1].ID.SchemeID) + }) + t.Run("document type extension", func(t *testing.T) { env, err := loadTestEnvelope("invoice-minimal.json") require.NoError(t, err) diff --git a/totals_test.go b/totals_test.go index 66b76635..b81cbcea 100644 --- a/totals_test.go +++ b/totals_test.go @@ -24,4 +24,23 @@ func TestNewTotals(t *testing.T) { assert.Equal(t, "VAT", doc.TaxTotal[0].TaxSubtotal[0].TaxCategory.TaxScheme.ID) assert.Equal(t, "21.0", *doc.TaxTotal[0].TaxSubtotal[0].TaxCategory.Percent) }) + + t.Run("exemption reason from legal note with reverse-charge tag", func(t *testing.T) { + doc, err := testInvoiceFrom("peppol/peppol-reverse-charge.json") + require.NoError(t, err) + + subtotal := doc.TaxTotal[0].TaxSubtotal[0] + require.NotNil(t, subtotal.TaxCategory.TaxExemptionReason) + assert.Equal(t, "Reverse Charge / Umkehr der Steuerschuld.", *subtotal.TaxCategory.TaxExemptionReason) + require.NotNil(t, subtotal.TaxCategory.TaxExemptionReasonCode) + assert.Equal(t, "VATEX-EU-AE", *subtotal.TaxCategory.TaxExemptionReasonCode) + }) + + t.Run("no exemption reason without reverse-charge tag", func(t *testing.T) { + doc, err := testInvoiceFrom("peppol/peppol-1.json") + require.NoError(t, err) + + subtotal := doc.TaxTotal[0].TaxSubtotal[0] + assert.Nil(t, subtotal.TaxCategory.TaxExemptionReason) + }) }