Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
4 changes: 2 additions & 2 deletions data/rules/org.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
8 changes: 4 additions & 4 deletions org/attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"`
Expand All @@ -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",
Expand Down
22 changes: 15 additions & 7 deletions org/attribute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
Loading