Skip to content

Commit

Permalink
Merge branch 'main' into addon-verifactu
Browse files Browse the repository at this point in the history
  • Loading branch information
apardods committed Nov 11, 2024
2 parents 8f0cee7 + dc6100f commit 580b5e4
Show file tree
Hide file tree
Showing 18 changed files with 314 additions and 50 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p

### Added

- `org`: `Address` now includes a `state` code, for countries that require them.
- `es-tbai-v1`: normalize address information to automatically add new `es-tbai-region` extension to invoices.
- `org`: `Inbox` now supports `email` field, with auto-normalization of URLs and emails in the `code` field.
- `currency`: Exchange rate "source" field.

### Changes

- Moved regime examples into single `/examples` folder.
- `org`: `Address`, `code` for the post code is now typed as a `cbc.Code`, like the new `state` field.

### Fixes

- `cal`: Fixing json schema issue with date times.

## [v0.204.1]

Expand Down
8 changes: 4 additions & 4 deletions cal/date_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,12 @@ func (dt DateTime) TimeZ() time.Time {
// JSONSchema returns a custom json schema for the date time.
func (DateTime) JSONSchema() *jsonschema.Schema {
return &jsonschema.Schema{
Type: "string",
Format: "date-time",
Title: "Date Time",
Type: "string",
Title: "Date Time",
Pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}$",
Description: here.Doc(`
Civil date time in simplified ISO format with no time zone
information, for example: 2021-05-26T13:45:00
nor location information, for example: 2021-05-26T13:45:00
`),
}
}
Expand Down
8 changes: 8 additions & 0 deletions cal/date_time_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,11 @@ func TestDateTimeOf(t *testing.T) {
d := cal.DateTimeOf(x)
assert.Equal(t, "2023-07-28T12:12:01", d.String())
}

func TestJSONSchema(t *testing.T) {
data := []byte(`{"description":"Civil date time in simplified ISO format with no time zone\nnor location information, for example: 2021-05-26T13:45:00", "pattern":"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}$", "title":"Date Time", "type":"string"}`)
schema := cal.DateTime{}.JSONSchema()
out, err := json.Marshal(schema)
require.NoError(t, err)
assert.JSONEq(t, string(data), string(out))
}
15 changes: 11 additions & 4 deletions currency/exchange_rate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/invopop/gobl/cal"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/num"
"github.com/invopop/validation"
)
Expand Down Expand Up @@ -32,20 +33,26 @@ type ExchangeRate struct {
From Code `json:"from" jsonschema:"title=From"`
// Currency code this exchange rate will convert into.
To Code `json:"to" jsonschema:"title=To"`
// At represents the effective date and time at which the exchange rate
// is determined by the source. The time may be zero if referring to a
// specific day only.
At *cal.DateTime `json:"at,omitempty" jsonschema:"title=At"`
// Source key provides a reference to the source the exchange rate was
// obtained from. Typically this will be determined by an application
// used to update exchange rates automatically.
Source cbc.Key `json:"source,omitempty" jsonschema:"title=Source"`
// How much is 1 of the "from" currency worth in the "to" currency.
Amount num.Amount `json:"amount" jsonschema:"title=Amount"`
// At represents the date and time (which may be 00:00:00) when the
// currency rate amount was determined.
At *cal.DateTime `json:"at,omitempty" jsonschema:"title=At"`
}

// Validate ensures the content of the exchange rate looks good.
func (er *ExchangeRate) Validate() error {
return validation.ValidateStruct(er,
validation.Field(&er.From, validation.Required),
validation.Field(&er.To, validation.Required),
validation.Field(&er.At, cal.DateTimeNotZero()),
validation.Field(&er.Source),
validation.Field(&er.Amount, num.Positive),
validation.Field(&er.At),
)
}

Expand Down
4 changes: 2 additions & 2 deletions data/schemas/cal/date-time.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"$defs": {
"DateTime": {
"type": "string",
"format": "date-time",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}$",
"title": "Date Time",
"description": "Civil date time in simplified ISO format with no time zone\ninformation, for example: 2021-05-26T13:45:00"
"description": "Civil date time in simplified ISO format with no time zone\nnor location information, for example: 2021-05-26T13:45:00"
}
}
}
15 changes: 10 additions & 5 deletions data/schemas/currency/exchange-rate.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,20 @@
"title": "To",
"description": "Currency code this exchange rate will convert into."
},
"at": {
"$ref": "https://gobl.org/draft-0/cal/date-time",
"title": "At",
"description": "At represents the effective date and time at which the exchange rate\nis determined by the source. The time may be zero if referring to a\nspecific day only."
},
"source": {
"$ref": "https://gobl.org/draft-0/cbc/key",
"title": "Source",
"description": "Source key provides a reference to the source the exchange rate was\nobtained from. Typically this will be determined by an application\nused to update exchange rates automatically."
},
"amount": {
"$ref": "https://gobl.org/draft-0/num/amount",
"title": "Amount",
"description": "How much is 1 of the \"from\" currency worth in the \"to\" currency."
},
"at": {
"$ref": "https://gobl.org/draft-0/cal/date-time",
"title": "At",
"description": "At represents the date and time (which may be 00:00:00) when the\ncurrency rate amount was determined."
}
},
"type": "object",
Expand Down
11 changes: 8 additions & 3 deletions data/schemas/org/address.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,20 @@
"locality": {
"type": "string",
"title": "Locality",
"description": "Village, town, district, or city, typically inside a region."
"description": "Name of a village, town, district, or city, typically inside a region."
},
"region": {
"type": "string",
"title": "Region",
"description": "Province, county, or state, inside a country."
"description": "Name of a city, province, county, or state, inside a country."
},
"state": {
"$ref": "https://gobl.org/draft-0/cbc/code",
"title": "State",
"description": "State or province code for countries that require it."
},
"code": {
"type": "string",
"$ref": "https://gobl.org/draft-0/cbc/code",
"title": "Code",
"description": "Post or ZIP code."
},
Expand Down
9 changes: 7 additions & 2 deletions data/schemas/org/inbox.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@
"code": {
"$ref": "https://gobl.org/draft-0/cbc/code",
"title": "Code",
"description": "Code or ID that identifies the Inbox."
"description": "Code or ID that identifies the Inbox. Mutually exclusive with URL and Email."
},
"url": {
"type": "string",
"title": "URL",
"description": "URL of the inbox that includes the protocol, server, and path. May\nbe used instead of the Code to identify the inbox."
"description": "URL of the inbox that includes the protocol, server, and path. May\nbe used instead of the Code to identify the inbox. Mutually exclusive with\nCode and Email."
},
"email": {
"type": "string",
"title": "Email",
"description": "Email address for the inbox. Mutually exclusive with Code and URL."
},
"ext": {
"$ref": "https://gobl.org/draft-0/tax/extensions",
Expand Down
4 changes: 4 additions & 0 deletions examples/it/hotel-b2g.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
{
"key": "it-sdi-code",
"code": "M5UXCR5"
},
{
"key": "it-sdi-pec",
"code": "inbox@example.com"
}
],
"addresses": [
Expand Down
2 changes: 2 additions & 0 deletions examples/it/hotel-custom.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ customer:
inboxes:
- key: it-sdi-code
code: M5UXCR5
- key: it-sdi-pec
email: "inbox@example.com"
addresses:
- num: "23"
street: Via dei Mille
Expand Down
6 changes: 5 additions & 1 deletion examples/it/out/hotel-b2g.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"uuid": "8a51fd30-2a27-11ee-be56-0242ac120002",
"dig": {
"alg": "sha256",
"val": "0abfdf85e257a1f08d3fb67873c8543921aaf49a3c53241eb040d568c70eb021"
"val": "feb8dd76847885f7d513e188e12c5213073c8348c942929c24692ec65e46cf7c"
}
},
"doc": {
Expand Down Expand Up @@ -65,6 +65,10 @@
{
"key": "it-sdi-code",
"code": "M5UXCR5"
},
{
"key": "it-sdi-pec",
"email": "inbox@example.com"
}
],
"addresses": [
Expand Down
6 changes: 5 additions & 1 deletion examples/it/out/hotel-custom.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"uuid": "8a51fd30-2a27-11ee-be56-0242ac120002",
"dig": {
"alg": "sha256",
"val": "0abfdf85e257a1f08d3fb67873c8543921aaf49a3c53241eb040d568c70eb021"
"val": "feb8dd76847885f7d513e188e12c5213073c8348c942929c24692ec65e46cf7c"
}
},
"doc": {
Expand Down Expand Up @@ -65,6 +65,10 @@
{
"key": "it-sdi-code",
"code": "M5UXCR5"
},
{
"key": "it-sdi-pec",
"email": "inbox@example.com"
}
],
"addresses": [
Expand Down
31 changes: 28 additions & 3 deletions org/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org

import (
"context"
"strings"

"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/l10n"
Expand Down Expand Up @@ -33,12 +34,14 @@ type Address struct {
Street string `json:"street,omitempty" jsonschema:"title=Street"`
// Additional street address details.
StreetExtra string `json:"street_extra,omitempty" jsonschema:"title=Extended Street"`
// Village, town, district, or city, typically inside a region.
// Name of a village, town, district, or city, typically inside a region.
Locality string `json:"locality,omitempty" jsonschema:"title=Locality"`
// Province, county, or state, inside a country.
// Name of a city, province, county, or state, inside a country.
Region string `json:"region,omitempty" jsonschema:"title=Region"`
// State or province code for countries that require it.
State cbc.Code `json:"state,omitempty" jsonschema:"title=State"`
// Post or ZIP code.
Code string `json:"code,omitempty" jsonschema:"title=Code"`
Code cbc.Code `json:"code,omitempty" jsonschema:"title=Code"`
// ISO country code.
Country l10n.ISOCountryCode `json:"country,omitempty" jsonschema:"title=Country"`
// When the postal address is not sufficient, coordinates help locate the address more precisely.
Expand All @@ -47,6 +50,26 @@ type Address struct {
Meta cbc.Meta `json:"meta,omitempty" jsonschema:"title=Meta"`
}

// Normalize will perform basic normalization of the address's data.
func (a *Address) Normalize(normalizers tax.Normalizers) {
if a == nil {
return
}
uuid.Normalize(&a.UUID)
a.PostOfficeBox = strings.TrimSpace(a.PostOfficeBox)
a.Number = strings.TrimSpace(a.Number)
a.Floor = strings.TrimSpace(a.Floor)
a.Block = strings.TrimSpace(a.Block)
a.Door = strings.TrimSpace(a.Door)
a.Street = strings.TrimSpace(a.Street)
a.StreetExtra = strings.TrimSpace(a.StreetExtra)
a.Locality = strings.TrimSpace(a.Locality)
a.Region = strings.TrimSpace(a.Region)
a.State = cbc.NormalizeAlphanumericalCode(a.State)
a.Code = cbc.NormalizeCode(a.Code)
normalizers.Each(a)
}

// Validate checks that an address looks okay.
func (a *Address) Validate() error {
return a.ValidateWithContext(context.Background())
Expand All @@ -56,6 +79,8 @@ func (a *Address) Validate() error {
func (a *Address) ValidateWithContext(ctx context.Context) error {
return tax.ValidateStructWithContext(ctx, a,
validation.Field(&a.UUID),
validation.Field(&a.State),
validation.Field(&a.Code),
validation.Field(&a.Country),
validation.Field(&a.Coordinates),
validation.Field(&a.Meta),
Expand Down
75 changes: 75 additions & 0 deletions org/address_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org_test

import (
"testing"

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

func TestAddressNormalize(t *testing.T) {
t.Run("nil address", func(t *testing.T) {
var a *org.Address
assert.NotPanics(t, func() {
a.Normalize(nil)
})
})

t.Run("normalize fields", func(t *testing.T) {
a := &org.Address{
PostOfficeBox: " 20 ",
Number: " 12 ",
Floor: " 3 ",
Block: " A ",
Door: " 1 ",
Street: " Main St. ",
StreetExtra: " Apt. 3 ",
Locality: " Town ",
Region: " City ",
State: " MAD ",
Code: " HG12 2AB ",
}
a.Normalize(nil)

assert.Equal(t, "20", a.PostOfficeBox)
assert.Equal(t, "12", a.Number)
assert.Equal(t, "3", a.Floor)
assert.Equal(t, "A", a.Block)
assert.Equal(t, "1", a.Door)
assert.Equal(t, "Main St.", a.Street)
assert.Equal(t, "Apt. 3", a.StreetExtra)
assert.Equal(t, "Town", a.Locality)
assert.Equal(t, "City", a.Region)
assert.Equal(t, "MAD", a.State.String())
assert.Equal(t, "HG12 2AB", a.Code.String())
})
}

func TestAddressValidation(t *testing.T) {
t.Run("valid address", func(t *testing.T) {
a := &org.Address{
Number: "12",
Street: "Main St.",
Locality: "Town",
Region: "City",
State: "MAD",
Code: "HG12 2AB",
Country: "GB",
}
assert.NoError(t, a.Validate())
})

t.Run("invalid UUID", func(t *testing.T) {
a := &org.Address{
Number: "12",
Street: "Main St.",
Locality: "Town",
Region: "City",
State: "MAD",
Code: "HG12 2AB",
Country: "GB",
}
a.UUID = "invalid"
assert.ErrorContains(t, a.Validate(), "uuid: invalid UUID length: 7")
})
}
Loading

0 comments on commit 580b5e4

Please sign in to comment.