diff --git a/CHANGELOG.md b/CHANGELOG.md index bdf948f0a..2a0cc99e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Changed +- `org`: an `Attribute` may now hold more than one of the `text`, `code`, `amount`, or `date` values, where before exactly one was required, so that formats grouping related values under a single name can be mapped directly. At least one value is still needed. - `net`: requests to `/who` and `/inbox` without a valid request token are rejected with 401; servers may keep an audit log of requester identities. `/who` responses are no longer publicly cacheable (`Cache-Control: private`); clients cache the verified party envelope locally instead. The `Fetcher` interface gains an `http.Header` parameter to carry the token. - `head`/`net`: signed `iss`/`aud`/`verifier` claims and request-token claims now carry bare GOBL Net addresses (FQDNs) instead of `gobl:` URIs — the scheme carried no information inside the protocol, and an FQDN can never contain a colon, so URI forms could still be admitted unambiguously later. `head.SigningPayload` fields and the `WithIssuer`/`WithAudience`/`WithVerifier` options are plain strings (use `Address.String()`); `Client.VerifyEnvelope` takes an `expectedAud net.Address`. The `gobl:` scheme remains where multiple schemes coexist: `org.Endpoint` URIs and the unsigned header `from`/`to`. diff --git a/data/rules/org.json b/data/rules/org.json index bed7ef3e2..eaf7ba355 100644 --- a/data/rules/org.json +++ b/data/rules/org.json @@ -44,8 +44,8 @@ }, { "id": "GOBL-ORG-ATTRIBUTE-02", - "desc": "attribute must have exactly one of the text, code, amount, or date values", - "tests": "(Text == \"\" ? 0 : 1) + (string(Code) == \"\" ? 0 : 1) + (Amount == nil ? 0 : 1) + (Date == nil ? 0 : 1) == 1" + "desc": "attribute must have at least one of the text, code, amount, or date values", + "tests": "(Text == \"\" ? 0 : 1) + (string(Code) == \"\" ? 0 : 1) + (Amount == nil ? 0 : 1) + (Date == nil ? 0 : 1) \u003e= 1" } ], "subsets": [ diff --git a/org/attribute.go b/org/attribute.go index ae7c0b2a9..44e6322d7 100644 --- a/org/attribute.go +++ b/org/attribute.go @@ -151,7 +151,7 @@ var AttributeKeyDefinitions = []*cbc.Definition{ // Attribute describes a named feature or property of the parent object, // such as the color or size of an item. Attributes are identified by -// either a key or a type, and hold exactly one of the text, code, amount, +// either a key or a type, and hold at least one of the text, code, amount, // or date value fields. type Attribute struct { // Label for the attribute, used for presentation in converted outputs @@ -168,7 +168,7 @@ type Attribute struct { // and customer. Type cbc.Code `json:"type,omitempty" jsonschema:"title=Type"` - // Value fields; exactly one must be provided. + // Value fields; at least one must be provided. // Text value of the attribute. Text string `json:"text,omitempty" jsonschema:"title=Text"` @@ -188,8 +188,8 @@ func attributeRules() *rules.Set { rules.Assert("01", "attribute must have either a key or a type, but not both", is.Expr(`(string(Key) == "") != (string(Type) == "")`), ), - rules.Assert("02", "attribute must have exactly one of the text, code, amount, or date values", - is.Expr(`(Text == "" ? 0 : 1) + (string(Code) == "" ? 0 : 1) + (Amount == nil ? 0 : 1) + (Date == nil ? 0 : 1) == 1`), + rules.Assert("02", "attribute must have at least one of the text, code, amount, or date values", + is.Expr(`(Text == "" ? 0 : 1) + (string(Code) == "" ? 0 : 1) + (Amount == nil ? 0 : 1) + (Date == nil ? 0 : 1) >= 1`), ), rules.When(is.Expr(`string(Unit) != ""`), rules.Assert("03", "attribute unit may only be used alongside an amount", diff --git a/org/attribute_test.go b/org/attribute_test.go index e5ae022a4..1f71d5c95 100644 --- a/org/attribute_test.go +++ b/org/attribute_test.go @@ -74,27 +74,35 @@ func TestAttributeValidation(t *testing.T) { } assert.ErrorContains(t, rules.Validate(a), "attribute must have either a key or a type, but not both") }) - t.Run("missing value", func(t *testing.T) { + t.Run("valid with text and date", func(t *testing.T) { a := &org.Attribute{ - Key: org.AttributeKeyColor, + Type: "INTENTO", + Text: "08060120341234567-000001", + Date: cal.NewDate(2026, time.March, 10), } - assert.ErrorContains(t, rules.Validate(a), "attribute must have exactly one of the text, code, amount, or date values") + assert.NoError(t, rules.Validate(a)) }) - t.Run("multiple values", func(t *testing.T) { + t.Run("valid with text and amount", func(t *testing.T) { a := &org.Attribute{ Key: org.AttributeKeyWeight, Text: "200g", Amount: num.NewAmount(200, 0), } - assert.ErrorContains(t, rules.Validate(a), "attribute must have exactly one of the text, code, amount, or date values") + assert.NoError(t, rules.Validate(a)) }) - t.Run("text and code", func(t *testing.T) { + t.Run("valid with text and code", func(t *testing.T) { a := &org.Attribute{ Key: org.AttributeKeyColor, Text: "Black", Code: "RAL 9005", } - assert.ErrorContains(t, rules.Validate(a), "attribute must have exactly one of the text, code, amount, or date values") + assert.NoError(t, rules.Validate(a)) + }) + t.Run("missing value", func(t *testing.T) { + a := &org.Attribute{ + Key: org.AttributeKeyColor, + } + assert.ErrorContains(t, rules.Validate(a), "attribute must have at least one of the text, code, amount, or date values") }) t.Run("unit without amount", func(t *testing.T) { a := &org.Attribute{