From ee30f9519f2bf137abd7c1724812c3720c02c9af Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Tue, 25 Feb 2025 11:33:34 +0000 Subject: [PATCH 1/2] Fix pay terms NA option and tests --- pay/terms.go | 20 +++++++++--- pay/terms_test.go | 80 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/pay/terms.go b/pay/terms.go index c17bcc7a..5665463d 100644 --- a/pay/terms.go +++ b/pay/terms.go @@ -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 @@ -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 @@ -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"}, @@ -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. @@ -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 { @@ -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), ) } @@ -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), ) diff --git a/pay/terms_test.go b/pay/terms_test.go index 1497b8e3..c4a51050 100644 --- a/pay/terms_test.go +++ b/pay/terms_test.go @@ -1,4 +1,4 @@ -package pay +package pay_test import ( "testing" @@ -6,11 +6,15 @@ import ( "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") @@ -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), @@ -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), @@ -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) +} From 9b25f3731d4d6134f078607b495e282e613e8ffc Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Tue, 25 Feb 2025 11:35:16 +0000 Subject: [PATCH 2/2] Updating changelog and schemas --- CHANGELOG.md | 4 ++++ data/schemas/pay/terms.json | 15 ++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3a4fa10..6beae519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,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 diff --git a/data/schemas/pay/terms.json b/data/schemas/pay/terms.json index 061cea7f..05356579 100644 --- a/data/schemas/pay/terms.json +++ b/data/schemas/pay/terms.json @@ -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", @@ -92,6 +87,11 @@ "const": "delivery", "title": "Delivery", "description": "Payment on Delivery" + }, + { + "const": "undefined", + "title": "Undefined", + "description": "Not yet defined" } ], "title": "Key", @@ -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",