Skip to content

Commit

Permalink
Correction Option Schema support
Browse files Browse the repository at this point in the history
  • Loading branch information
samlown committed Sep 20, 2023
1 parent 98cd5ce commit a22e156
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 19 deletions.
81 changes: 81 additions & 0 deletions bill/invoice_correct.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import (
"errors"
"fmt"

"github.com/iancoleman/orderedmap"
"github.com/invopop/gobl/build"
"github.com/invopop/gobl/cal"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/head"
"github.com/invopop/gobl/schema"
"github.com/invopop/gobl/tax"
"github.com/invopop/jsonschema"
)

// CorrectionOptions defines a structure used to pass configuration options
Expand Down Expand Up @@ -115,6 +118,84 @@ var Debit schema.Option = func(o interface{}) {
opts.Debit = true
}

// CorrectionOptionsSchema provides a dynamic JSON schema of the options
// that can be used on the invoice in order to correct it. Data is
// extracted from the tax regime associated with the supplier.
func (inv *Invoice) CorrectionOptionsSchema() (interface{}, error) {
r := taxRegimeFor(inv.Supplier)
if r == nil {
return nil, nil
}
cd := r.CorrectionDefinitionFor(ShortSchemaInvoice)

schema := new(jsonschema.Schema)

// try to load the pre-generated schema, this is just way more efficient
// than trying to generate the configuration options manually.
data, err := build.Content.ReadFile("schemas/bill/correction-options.json")
if err != nil {
return nil, fmt.Errorf("loading schema option data: %w", err)
}
if err := json.Unmarshal(data, schema); err != nil {
return nil, fmt.Errorf("unmarshalling options schema: %w", err)
}
schema = schema.Definitions["CorrectionOptions"]

// Improve the quality of the schema
schema.Required = append(schema.Required, "credit")
if cd != nil {
if cd.ReasonRequired {
schema.Required = append(schema.Required, "reason")
}

// These methods are quite ugly as the jsonschema was not designed
// for being able to load documents.
if len(cd.Methods) > 0 {
schema.Required = append(schema.Required, "method")
if prop, ok := schema.Properties.Get("method"); ok {
ps := prop.(orderedmap.OrderedMap)
oneOf := make([]*jsonschema.Schema, len(cd.Methods))
for i, v := range cd.Methods {
oneOf[i] = &jsonschema.Schema{
Const: v.Key.String(),
Title: v.Name.String(),
}
if !v.Desc.IsEmpty() {
oneOf[i].Description = v.Desc.String()
}
}
ps.Set("oneOf", oneOf)
schema.Properties.Set("method", ps)
}
}

if len(cd.Keys) > 0 {
schema.Required = append(schema.Required, "changes")
if prop, ok := schema.Properties.Get("changes"); ok {
ps := prop.(orderedmap.OrderedMap)
items, _ := ps.Get("items")
pi := items.(orderedmap.OrderedMap)

oneOf := make([]*jsonschema.Schema, len(cd.Keys))
for i, v := range cd.Keys {
oneOf[i] = &jsonschema.Schema{
Const: v.Key.String(),
Title: v.Name.String(),
}
if !v.Desc.IsEmpty() {
oneOf[i].Description = v.Desc.String()
}
}
pi.Set("oneOf", oneOf)
ps.Set("items", pi)
schema.Properties.Set("changes", ps)
}
}
}

return schema, nil
}

// Correct moves key fields of the current invoice to the preceding
// structure and performs any regime specific actions defined by the
// regime's configuration.
Expand Down
27 changes: 27 additions & 0 deletions bill/invoice_correct_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package bill_test

import (
"encoding/json"
"testing"

"github.com/iancoleman/orderedmap"
"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/cal"
"github.com/invopop/gobl/cbc"
Expand All @@ -14,6 +16,7 @@ import (
"github.com/invopop/gobl/regimes/common"
"github.com/invopop/gobl/regimes/es"
"github.com/invopop/gobl/tax"
"github.com/invopop/jsonschema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -141,6 +144,30 @@ func TestCorrectWithOptions(t *testing.T) {
assert.Equal(t, i.Totals.Payable.String(), "-900.00")
}

func TestCorrectionOptionsSchema(t *testing.T) {
inv := testInvoiceESForCorrection(t)
out, err := inv.CorrectionOptionsSchema()
require.NoError(t, err)

schema, ok := out.(*jsonschema.Schema)
require.True(t, ok)

assert.Len(t, schema.Properties.Keys(), 7)

mtd, ok := schema.Properties.Get("method")
require.True(t, ok)
pm := mtd.(orderedmap.OrderedMap)
assert.Len(t, pm.Keys(), 4)

exp := `{"$ref":"https://gobl.org/draft-0/cbc/key","title":"Method","description":"Correction method as defined by the tax regime.","oneOf":[{"const":"complete","title":"Complete"},{"const":"partial","title":"Corrected items only"},{"const":"discount","title":"Bulk deal in a given period"},{"const":"authorized","title":"Authorized by the Tax Agency"}]`

data, err := json.Marshal(pm)
require.NoError(t, err)
if !assert.JSONEq(t, exp, string(data)) {
t.Log(string(data))
}
}

func TestCorrectWithData(t *testing.T) {
i := testInvoiceESForCorrection(t)
data := []byte(`{"credit":true,"reason":"test refund"}`)
Expand Down
14 changes: 14 additions & 0 deletions envelope.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,17 @@ func (e *Envelope) Correct(opts ...schema.Option) (*Envelope, error) {
// Create a completely new envelope with a new set of data.
return Envelop(nd)
}

// CorrectionOptionsSchema will attempt to provide a corrective options JSON Schema
// that can be used to generate a JSON object to send when correcting a document.
// If none are available, the result will be nil.
func (e *Envelope) CorrectionOptionsSchema() (interface{}, error) {
if e.Document == nil {
return nil, ErrNoDocument
}
opts, err := e.Document.CorrectionOptionsSchema()
if err != nil {
return nil, err
}
return opts, nil
}
16 changes: 13 additions & 3 deletions i18n/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ const (
// String provides a simple map of locales to texts.
type String map[Lang]string

// String provides a single string from the map using the
// language requested or resorting to the default.
func (s String) String(lang Lang) string {
// In provides a single string from the map using the
// language requested or resorts to the default.
func (s String) In(lang Lang) string {
if v, ok := s[lang]; ok {
return v
}
return s.String()
}

// String returns the default language string or first entry found.
func (s String) String() string {
if v, ok := s[defaultLanguage]; ok {
return v
}
Expand All @@ -24,6 +29,11 @@ func (s String) String(lang Lang) string {
return ""
}

// IsEmpty returns true if the string map is empty.
func (s String) IsEmpty() bool {
return len(s) == 0
}

// JSONSchema returns the json schema definition
func (String) JSONSchema() *jsonschema.Schema {
return &jsonschema.Schema{
Expand Down
19 changes: 7 additions & 12 deletions i18n/string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/invopop/gobl/i18n"
"github.com/stretchr/testify/assert"
)

func TestI18nString(t *testing.T) {
Expand All @@ -12,20 +13,14 @@ func TestI18nString(t *testing.T) {
"es": "Prueba",
}

if x := s.String("en"); x != "Test" {
t.Errorf("Unexpected string result: %v", x)
}
if x := s.String("es"); x != "Prueba" {
t.Errorf("Unexpected string result: %v", x)
}
if x := s.String("fo"); x != "Test" {
t.Errorf("Unexpected string result: %v", x)
}
assert.Equal(t, "Test", s.In("en"))
assert.Equal(t, "Prueba", s.In("es"))
assert.Equal(t, "Test", s.In("fo"))
assert.Equal(t, "Test", s.String())

snd := i18n.String{
i18n.AA: "Foo",
}
if x := snd.String("en"); x != "Foo" {
t.Errorf("Unexpected string result: %v", x)
}
assert.Equal(t, "Foo", snd.In("en"))
assert.Equal(t, "Foo", snd.String())
}
20 changes: 18 additions & 2 deletions regimes/co/preceding.go → regimes/co/corrections.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const (
var correctionMethodList = []*tax.KeyDefinition{
{
Key: CorrectionMethodKeyPartial,
Name: i18n.String{
i18n.EN: "Partial refund",
i18n.ES: "Devolución parcial",
},
Desc: i18n.String{
i18n.EN: "Partial refund of part of the goods or services.",
i18n.ES: "Devolución de parte de los bienes; no aceptación de partes del servicio.",
Expand All @@ -29,6 +33,10 @@ var correctionMethodList = []*tax.KeyDefinition{
},
{
Key: CorrectionMethodKeyRevoked,
Name: i18n.String{
i18n.EN: "Revoked",
i18n.ES: "Anulación",
},
Desc: i18n.String{
i18n.EN: "Previous document has been completely cancelled.",
i18n.ES: "Anulación de la factura anterior.",
Expand All @@ -39,6 +47,10 @@ var correctionMethodList = []*tax.KeyDefinition{
},
{
Key: CorrectionMethodKeyDiscount,
Name: i18n.String{
i18n.EN: "Discount",
i18n.ES: "Descuento",
},
Desc: i18n.String{
i18n.EN: "Partial or total discount.",
i18n.ES: "Rebaja o descuento parcial o total.",
Expand All @@ -49,9 +61,13 @@ var correctionMethodList = []*tax.KeyDefinition{
},
{
Key: CorrectionMethodKeyPriceAdjustment,
Name: i18n.String{
i18n.EN: "Adjustment",
i18n.ES: "Ajuste",
},
Desc: i18n.String{
i18n.EN: "Ajuste de precio.",
i18n.ES: "Price adjustment.",
i18n.EN: "Price adjustment.",
i18n.ES: "Ajuste de precio.",
},
Map: cbc.CodeMap{
KeyDIAN: "4",
Expand Down
4 changes: 2 additions & 2 deletions regimes/co/tax_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ func normalizePartyWithTaxIdentity(p *org.Party) error {
return nil
}
a := p.Addresses[0]
a.Locality = z.Locality.String(i18n.ES)
a.Region = z.Region.String(i18n.ES)
a.Locality = z.Locality.In(i18n.ES)
a.Region = z.Region.In(i18n.ES)
}
}
return nil
Expand Down
15 changes: 15 additions & 0 deletions schema/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Calculable interface {
// corrected.
type Correctable interface {
Correct(...Option) error
CorrectionOptionsSchema() (interface{}, error)
}

// NewObject instantiates an Object wrapper around the provided payload.
Expand Down Expand Up @@ -107,6 +108,20 @@ func (d *Object) Correct(opts ...Option) error {
return nil
}

// CorrectionOptionsSchema provides a schema with the correction options available
// for the schema, if available.
func (d *Object) CorrectionOptionsSchema() (interface{}, error) {
pl, ok := d.payload.(Correctable)
if !ok {
return nil, nil
}
res, err := pl.CorrectionOptionsSchema()
if err != nil {
return nil, err
}
return res, nil
}

// Insert places the provided object inside the document and looks up the schema
// information to ensure it is known.
func (d *Object) insert(payload interface{}) error {
Expand Down

0 comments on commit a22e156

Please sign in to comment.