Skip to content

Commit

Permalink
Merge pull request #473 from invopop/fix-pay-terms-na
Browse files Browse the repository at this point in the history
Fix pay terms na
  • Loading branch information
samlown authored Feb 28, 2025
2 parents 0671e0f + 9b25f37 commit e309e5b
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 17 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ Each document class has a subset of types to cover multiple situations. Its been
- `bill`: renaming `Receipt` to `Payment`, and associated payment types to simply `advice` and `receipt`.
- `org`: `Item` price is now a pointer and optional, so that items without prices can be used in `bill.Order` and `bill.Delivery` documents. `bill.Invoice` continues to validate for the presence of an item's price, as expected.

### Fixed

- `pay`: `Terms`, replaced `NA` option with explicit `undefined` key, to avoid defining empty constants in JSON.

## [v0.210.0] - 2025-02-19

### Added
Expand Down
15 changes: 10 additions & 5 deletions data/schemas/pay/terms.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,6 @@
"key": {
"$ref": "https://gobl.org/draft-0/cbc/key",
"oneOf": [
{
"const": "",
"title": "NA",
"description": "Not yet defined"
},
{
"const": "end-of-month",
"title": "End of Month",
Expand Down Expand Up @@ -92,6 +87,11 @@
"const": "delivery",
"title": "Delivery",
"description": "Payment on Delivery"
},
{
"const": "undefined",
"title": "Undefined",
"description": "Not yet defined"
}
],
"title": "Key",
Expand All @@ -114,6 +114,11 @@
"type": "string",
"title": "Notes",
"description": "Description of the conditions for payment."
},
"ext": {
"$ref": "https://gobl.org/draft-0/tax/extensions",
"title": "Extensions",
"description": "Extensions to the terms for local codes."
}
},
"type": "object",
Expand Down
20 changes: 16 additions & 4 deletions pay/terms.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ type Terms struct {
DueDates []*DueDate `json:"due_dates,omitempty" jsonschema:"title=Due Dates"`
// Description of the conditions for payment.
Notes string `json:"notes,omitempty" jsonschema:"title=Notes"`
// Extensions to the terms for local codes.
Ext tax.Extensions `json:"ext,omitempty" jsonschema:"title=Extensions"`
}

// Pre-defined Payment Terms based on UNTDID 4279
const (
// None defined
TermKeyNA cbc.Key = ""
// End of Month
TermKeyEndOfMonth cbc.Key = "end-of-month"
// Due on a specific date
Expand All @@ -47,6 +47,8 @@ const (
TermKeyAdvanced cbc.Key = "advanced"
// Payment on Delivery
TermKeyDelivery cbc.Key = "delivery"
// Not yet defined
TermKeyUndefined cbc.Key = "undefined"
)

// TermKeyDef holds a definition of a single payment term key
Expand All @@ -64,7 +66,6 @@ type TermKeyDef struct {
// TermKeyDefinitions includes all the currently accepted
// GOBL Payment Term definitions.
var TermKeyDefinitions = []TermKeyDef{
{TermKeyNA, "NA", "Not yet defined", "16"},
{TermKeyEndOfMonth, "End of Month", "End of month", "2"},
{TermKeyDueDate, "Due Date", "Due on a specific date", "3"},
{TermKeyDeferred, "Deferred", "Deferred until after the due date", "4"},
Expand All @@ -74,6 +75,7 @@ var TermKeyDefinitions = []TermKeyDef{
{TermKeyPending, "Pending", "Seller to advise buyer in separate transaction", "13"},
{TermKeyAdvanced, "Advanced", "Payment made in advance", "32"},
{TermKeyDelivery, "Delivery", "Payment on Delivery", "52"}, // Cash on Delivery (COD)
{TermKeyUndefined, "Undefined", "Not yet defined", "16"},
}

// DueDate contains an amount that should be paid by the given date.
Expand All @@ -85,6 +87,15 @@ type DueDate struct {
Currency currency.Code `json:"currency,omitempty" jsonschema:"title=Currency,description=If different from the parent document's base currency."`
}

// Normalize will try to normalize the payment terms.
func (t *Terms) Normalize(normalizers tax.Normalizers) {
if t == nil {
return
}
t.Ext = tax.CleanExtensions(t.Ext)
normalizers.Each(t)
}

// UNTDID4279 returns the UNTDID 4279 code associated with the terms key.
func (t *Terms) UNTDID4279() cbc.Code {
for _, v := range TermKeyDefinitions {
Expand Down Expand Up @@ -119,6 +130,7 @@ func (t *Terms) ValidateWithContext(ctx context.Context) error {
return tax.ValidateStructWithContext(ctx, t,
validation.Field(&t.Key, isValidTermKey),
validation.Field(&t.DueDates),
validation.Field(&t.Ext),
)
}

Expand All @@ -136,7 +148,7 @@ func validTermKeys() []interface{} {
func (dd *DueDate) Validate() error {
return validation.ValidateStruct(dd,
validation.Field(&dd.Date, validation.Required),
validation.Field(&dd.Amount, validation.Required),
validation.Field(&dd.Amount, validation.Required, num.NotZero),
validation.Field(&dd.Percent),
validation.Field(&dd.Currency),
)
Expand Down
80 changes: 72 additions & 8 deletions pay/terms_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package pay
package pay_test

import (
"testing"

"github.com/invopop/gobl/cal"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/num"
"github.com/invopop/gobl/pay"
"github.com/invopop/gobl/tax"
"github.com/invopop/jsonschema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestTermsValidation(t *testing.T) {
tm := new(Terms)
tm := new(pay.Terms)
tm.Key = cbc.Key("foo")
err := tm.Validate()
assert.Error(t, err, "expected validation error")
Expand All @@ -20,22 +24,67 @@ func TestTermsValidation(t *testing.T) {
assert.Error(t, err, "expected validation error")
assert.Contains(t, err.Error(), "key: must be a valid value")

tm.Key = TermKeyAdvanced
tm.Key = pay.TermKeyAdvanced
err = tm.Validate()
assert.NoError(t, err)

tm.Key = TermKeyNA
tm.Key = ""
err = tm.Validate()
assert.NoError(t, err)

t.Run("with due dates and missing amount", func(t *testing.T) {
tm := new(pay.Terms)
tm.Key = pay.TermKeyDueDate
tm.DueDates = []*pay.DueDate{
{
Date: cal.NewDate(2021, 11, 10),
},
}
err := tm.Validate()
assert.ErrorContains(t, err, "due_dates: (0: (amount: must not be zero.).)")
})
}

func TestTermsUNTDID4279(t *testing.T) {
t.Run("existing", func(t *testing.T) {
tm := new(pay.Terms)
tm.Key = pay.TermKeyEndOfMonth
assert.Equal(t, "2", tm.UNTDID4279().String())
})
t.Run("non-existing", func(t *testing.T) {
tm := new(pay.Terms)
tm.Key = cbc.Key("non-existing")
assert.Equal(t, cbc.CodeEmpty, tm.UNTDID4279())
})
}

func TestTermsNormalize(t *testing.T) {
t.Run("basic", func(t *testing.T) {
pt := &pay.Terms{
Key: pay.TermKeyUndefined,
Ext: tax.Extensions{
"random": "",
},
}
pt.Normalize(nil)
assert.Empty(t, pt.Ext)
assert.Equal(t, "undefined", pt.Key.String())
})
t.Run("nil", func(t *testing.T) {
var pt *pay.Terms
assert.NotPanics(t, func() {
pt.Normalize(nil)
})
})
}

func TestTermsCalculateDues(t *testing.T) {
sum := num.MakeAmount(10000, 2)
var terms *Terms
var terms *pay.Terms
zero := num.MakeAmount(0, 2)
terms.CalculateDues(zero, sum) // Should not panic
terms = new(Terms)
terms.DueDates = []*DueDate{
terms = new(pay.Terms)
terms.DueDates = []*pay.DueDate{
{
Date: cal.NewDate(2021, 11, 10),
Percent: num.NewPercentage(40, 2),
Expand All @@ -50,7 +99,7 @@ func TestTermsCalculateDues(t *testing.T) {
assert.Equal(t, num.MakeAmount(4000, 2), terms.DueDates[0].Amount)
assert.Equal(t, num.MakeAmount(6000, 2), terms.DueDates[1].Amount)

terms.DueDates = []*DueDate{
terms.DueDates = []*pay.DueDate{
{
Date: cal.NewDate(2021, 11, 10),
Amount: num.MakeAmount(40, 0),
Expand All @@ -59,3 +108,18 @@ func TestTermsCalculateDues(t *testing.T) {
terms.CalculateDues(zero, sum)
assert.Equal(t, "40.00", terms.DueDates[0].Amount.String(), "should normalize amounts for currency")
}

func TestTermsJSONSchemaExtend(t *testing.T) {
schema := &jsonschema.Schema{
Properties: jsonschema.NewProperties(),
}
schema.Properties.Set("key", &jsonschema.Schema{
Type: "string",
})
terms := &pay.Terms{}
terms.JSONSchemaExtend(schema)
prop, ok := schema.Properties.Get("key")
require.True(t, ok)
assert.Len(t, prop.OneOf, 10)
assert.Equal(t, cbc.Key("end-of-month"), prop.OneOf[0].Const)
}

0 comments on commit e309e5b

Please sign in to comment.