diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e8a4be..acb29a8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to GOBL will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). See also the [GOBL versions](https://docs.gobl.org/overview/versions) documentation site for more details. +## [Unreleased] + +### Change + +- `org.DocumentRef`: renamed `line` to `lines` that accepts an array of integers making it possible to define a selection of reference lines in another document as opposed to just one. + +### Added + +- `cbc.Code`: new `Join` and `JoinWith` methods to help concatenate codes. +- `it-sdi-v1`: added CIG and CUP identity type codes. + ## [v0.201.0] ### Fixed diff --git a/addons/it/sdi/identities.go b/addons/it/sdi/identities.go new file mode 100644 index 00000000..9a81a0f4 --- /dev/null +++ b/addons/it/sdi/identities.go @@ -0,0 +1,10 @@ +package sdi + +import "github.com/invopop/gobl/cbc" + +const ( + // IdentityTypeCUP defines code managed by the CIPE (Interministerial Committee for Economic Planning) which characterises every public investment project (Individual Project Code) + IdentityTypeCUP cbc.Code = "CUP" + // IdentityTypeCIG defines tender procedure identification code. + IdentityTypeCIG cbc.Code = "CIG" +) diff --git a/cbc/code.go b/cbc/code.go index 220df033..28da0ac7 100644 --- a/cbc/code.go +++ b/cbc/code.go @@ -5,10 +5,16 @@ import ( "regexp" "strings" + "github.com/invopop/gobl/pkg/here" "github.com/invopop/jsonschema" "github.com/invopop/validation" ) +const ( + // DefaultCodeSeparator is the default separator used to join codes. + DefaultCodeSeparator Code = "-" +) + // Code represents a string used to uniquely identify the data we're looking // at. We use "code" instead of "id", to reenforce the fact that codes should // be more easily set and used by humans within definitions than IDs or UUIDs. @@ -81,15 +87,37 @@ func (c Code) In(ary ...Code) bool { return false } +// Join returns a new code that is the result of joining the provided +// code with the current one using a default separator. +func (c Code) Join(c2 Code) Code { + return c.JoinWith(DefaultCodeSeparator, c2) +} + +// JoinWith returns a new code that is the result of joining the provided +// code with the current one using the provided separator. If any of the codes +// are empty, no separator will be added. +func (c Code) JoinWith(separator Code, c2 Code) Code { + if c == CodeEmpty { + return c2 + } + if c2 == CodeEmpty { + return c + } + return c + separator + c2 +} + // JSONSchema provides a representation of the struct for usage in Schema. func (Code) JSONSchema() *jsonschema.Schema { return &jsonschema.Schema{ - Type: "string", - Pattern: CodePattern, - Title: "Code", - MinLength: &CodeMinLength, - MaxLength: &CodeMaxLength, - Description: "Alphanumerical text identifier with upper-case letters, no whitespace, nor symbols.", + Type: "string", + Pattern: CodePattern, + Title: "Code", + MinLength: &CodeMinLength, + MaxLength: &CodeMaxLength, + Description: here.Doc(` + Alphanumerical text identifier with upper-case letters and limits on using + special characters or whitespace to separate blocks. + `), } } diff --git a/cbc/code_test.go b/cbc/code_test.go index c343f089..7de0649e 100644 --- a/cbc/code_test.go +++ b/cbc/code_test.go @@ -1,10 +1,14 @@ package cbc_test import ( + "encoding/json" "testing" "github.com/invopop/gobl/cbc" + "github.com/invopop/jsonschema" + "github.com/invopop/validation" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCodeIn(t *testing.T) { @@ -14,6 +18,30 @@ func TestCodeIn(t *testing.T) { assert.False(t, c.In("BAR", "DOM")) } +func TestCodeEmpty(t *testing.T) { + assert.Equal(t, cbc.Code(""), cbc.CodeEmpty) + assert.True(t, cbc.Code("").IsEmpty()) +} + +func TestCodeJoin(t *testing.T) { + t.Run("basic join", func(t *testing.T) { + c := cbc.Code("BAR") + assert.Equal(t, "BAR-FOO", c.Join("FOO").String()) + }) + t.Run("empty base join", func(t *testing.T) { + c := cbc.Code("") + assert.Equal(t, "FOO", c.Join("FOO").String()) + }) + t.Run("empty postfix join", func(t *testing.T) { + c := cbc.Code("BAR") + assert.Equal(t, "BAR", c.Join("").String()) + }) + t.Run("custom separator", func(t *testing.T) { + c := cbc.Code("BAR") + assert.Equal(t, "BAR|FOO", c.JoinWith("|", "FOO").String()) + }) +} + func TestNormalizeCode(t *testing.T) { tests := []struct { name string @@ -193,3 +221,95 @@ func TestCode_Validate(t *testing.T) { }) } } + +func TestCodeMap(t *testing.T) { + cm := cbc.CodeMap{ + "foo": cbc.Code("01"), + "bar": cbc.Code("02"), + } + t.Run("Has", func(t *testing.T) { + assert.True(t, cm.Has("foo")) + assert.True(t, cm.Has("foo", "bar")) + assert.False(t, cm.Has("dom")) + assert.False(t, cm.Has("foo", "dom")) + }) + + t.Run("validation", func(t *testing.T) { + assert.NoError(t, cm.Validate()) + cm2 := cbc.CodeMap{ + "Invalid": cbc.Code("01"), + } + assert.ErrorContains(t, cm2.Validate(), "Invalid: must be in a valid format") + }) +} + +func TestCodeMap_Equals(t *testing.T) { + cm := cbc.CodeMap{ + "foo": cbc.Code("01"), + "bar": cbc.Code("02"), + } + cm2 := cbc.CodeMap{ + "foo": cbc.Code("01"), + "bar": cbc.Code("02"), + } + cm3 := cbc.CodeMap{ + "foo": cbc.Code("01"), + "bar": cbc.Code("03"), + } + cm4 := cbc.CodeMap{ + "foo": cbc.Code("01"), + "bar": cbc.Code("02"), + "dom": cbc.Code("03"), + } + cm5 := cbc.CodeMap{ + "foo": cbc.Code("01"), + } + cm6 := cbc.CodeMap{ + "foo": cbc.Code("01"), + "dom": cbc.Code("02"), + } + assert.True(t, cm.Equals(cm2)) + assert.False(t, cm.Equals(cm3)) + assert.False(t, cm.Equals(cm4)) + assert.False(t, cm.Equals(cm5)) + assert.False(t, cm.Equals(cm6)) +} + +func TestCodeMapHas(t *testing.T) { + cm := cbc.CodeMap{ + "foo": cbc.Code("01"), + "bar": cbc.Code("02"), + } + err := validation.Validate(cm, cbc.CodeMapHas("foo", "bar")) + assert.NoError(t, err) + assert.ErrorContains(t, validation.Validate(cm, cbc.CodeMapHas("foo", "dom")), "dom: required.") + err = validation.Validate(nil, cbc.CodeMapHas("foo")) + assert.NoError(t, err) +} + +func TestCodeJSONSchema(t *testing.T) { + s := cbc.Code("").JSONSchema() + assert.Equal(t, "string", s.Type) + assert.Equal(t, "Code", s.Title) + assert.Equal(t, uint64(1), *s.MinLength) + assert.Equal(t, uint64(32), *s.MaxLength) +} + +func TestCodeMapJSONSchemaExtend(t *testing.T) { + eg := `{ + "type": "object", + "additionalProperties": { + "$ref": "https://gobl.org/draft-0/cbc/code" + }, + "description": "CodeMap is a map of keys to specific codes, useful to determine regime specific codes from their key counterparts." + }` + js := new(jsonschema.Schema) + require.NoError(t, json.Unmarshal([]byte(eg), js)) + + cm := cbc.CodeMap{} + cm.JSONSchemaExtend(js) + + assert.Nil(t, js.AdditionalProperties) + assert.Equal(t, 1, len(js.PatternProperties)) + assert.Equal(t, "https://gobl.org/draft-0/cbc/code", js.PatternProperties[cbc.KeyPattern].Ref) +} diff --git a/org/document_ref.go b/org/document_ref.go index b4e77769..90d97421 100644 --- a/org/document_ref.go +++ b/org/document_ref.go @@ -23,8 +23,8 @@ type DocumentRef struct { Series cbc.Code `json:"series,omitempty" jsonschema:"title=Series"` // Source document's code or other identifier. Code cbc.Code `json:"code" jsonschema:"title=Code"` - // Line index number inside the document, if relevant. - Line int `json:"line,omitempty" jsonschema:"title=Line"` + // Line index numbers inside the document, if relevant. + Lines []int `json:"lines,omitempty" jsonschema:"title=Lines"` // List of additional codes, IDs, or SKUs which can be used to identify the document or its contents, agreed upon by the supplier and customer. Identities []*Identity `json:"identities,omitempty" jsonschema:"title=Identities"` // Tax period in which the referred document had an effect required by some tax regimes and formats. diff --git a/org/identity.go b/org/identity.go index 01af392b..f05cfec3 100644 --- a/org/identity.go +++ b/org/identity.go @@ -12,13 +12,21 @@ import ( "github.com/invopop/validation" ) -// Default or common identity keys that may be used to identify a person or company. +// Common identity keys that may be used to identify something, like an item, document, +// person, organisation, or company. Ideally, these will only be used when no other +// more structured properties are available inside GOBL. The keys suggested here are +// non-binding and can be used as a reference for other implementations. const ( - IdentityKeyPassport cbc.Key = "passport" - IdentifyKeyNational cbc.Key = "national" - IdentityKeyForeign cbc.Key = "foreign" - IdentityKeyResident cbc.Key = "resident" - IdentityKeyOther cbc.Key = "other" + IdentityKeySKU cbc.Key = "sku" // stock code unit ID + IdentityKeyItem cbc.Key = "item" // item number + IdentityKeyOrder cbc.Key = "order" // order number or code + IdentityKeyAgreement cbc.Key = "agreement" // agreement number + IdentityKeyContract cbc.Key = "contract" // contract number + IdentityKeyPassport cbc.Key = "passport" // Passport number + IdentityKeyNational cbc.Key = "national" // National ID card number + IdentityKeyForeign cbc.Key = "foreign" // Foreigner ID card number + IdentityKeyResident cbc.Key = "resident" // Resident ID card number + IdentityKeyOther cbc.Key = "other" // Other ID card number ) // Identity is used to define a code for a specific context.