Skip to content

Commit

Permalink
Added Attachment to org package
Browse files Browse the repository at this point in the history
  • Loading branch information
samlown committed Dec 7, 2024
1 parent 152a0ac commit c3a06e2
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- `cbc`: `Key.Pop` method for splitting keys with sub-keys, e.g. `cbc.Key("a+b").Pop() == cbc.Key("a")`.
- `in`: added Indian regime
- `cef`: catalogue for CEF VATEX reason codes.
- `org`: `Attachment` new structure for dealing with attachments.
- `bill`: `Attachments` added to invoices.
- `eu-en16931-v2017`: addon now includes additional validation for attachment codes.

### Changed

Expand Down
3 changes: 3 additions & 0 deletions addons/eu/en16931/en16931.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/i18n"
"github.com/invopop/gobl/org"
"github.com/invopop/gobl/pay"
"github.com/invopop/gobl/pkg/here"
"github.com/invopop/gobl/tax"
Expand Down Expand Up @@ -69,6 +70,8 @@ func validate(doc any) error {
return validateBillInvoice(obj)
case *tax.Combo:
return validateTaxCombo(obj)
case *org.Attachment:
return validateOrgAttachment(obj)
}
return nil
}
15 changes: 15 additions & 0 deletions addons/eu/en16931/org.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package en16931

import (
"github.com/invopop/gobl/org"
"github.com/invopop/validation"
)

func validateOrgAttachment(a *org.Attachment) error {
return validation.ValidateStruct(a,
validation.Field(&a.Code,
validation.Required,
validation.Skip,
),
)
}
5 changes: 5 additions & 0 deletions bill/invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ type Invoice struct {

// Additional semi-structured data that doesn't fit into the body of the invoice.
Meta cbc.Meta `json:"meta,omitempty" jsonschema:"title=Meta"`

// Attachments provide additional information or supporting documents that are not included
// in the main document. It is important that attachments are not used for alternative
// versions of the PDF, for that, see "links" inside the envelope headers.
Attachments []*org.Attachment `json:"attachments,omitempty" jsonschema:"title=Attachments"`
}

// Validate checks to ensure the invoice is valid and contains all the information we need.
Expand Down
97 changes: 97 additions & 0 deletions org/attachment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org

import (
"context"

"github.com/invopop/gobl/cbc"
"github.com/invopop/gobl/dsig"
"github.com/invopop/gobl/tax"
"github.com/invopop/gobl/uuid"
"github.com/invopop/validation"
"github.com/invopop/validation/is"
)

// An Attachment provides a structure to be used to attach documents
// inside a GOBL document, either as a reference via a URL, or directly
// as a base64 encoded string.
//
// Attachments must not be used to include alternative versions of the
// same document, but rather include supporting documents or additional
// information that is not included in the main document.
//
// While it is possible to include the data directly in the JSON document,
// it is recommended to use the URL field to link to the document instead.
type Attachment struct {
uuid.Identify

// Key used to identify the attachment inside the document.
Key cbc.Key `json:"key,omitempty" jsonschema:"title=Key"`

// Code used to identify the payload of the attachment.
Code cbc.Code `json:"code,omitempty" jsonschema:"title=Code"`

// Filename of the attachment.
Name string `json:"name" jsonschema:"title=Name"`

// Details of why the attachment is being included and details on
// what it contains.
Description string `json:"description,omitempty" jsonschema:"title=Description"`

// URL of where to find the attachment. Prefer using this field
// over the Data field.
URL string `json:"url,omitempty" jsonschema:"title=URL,format=uri"`

// Digest is used to verify the integrity of the attachment
// when downloaded from the URL.
Digest *dsig.Digest `json:"digest,omitempty" jsonschema:"title=Digest"`

// MIME type of the attachment.
MIME string `json:"mime,omitempty" jsonschema:"title=MIME Type"`

// Data is the base64 encoded data of the attachment directly embedded
// inside the GOBL document. This should only be used when the URL cannot
// be used as it can dramatically increase the size of the JSON
// document, thus effectiving usability and performance.
Data []byte `json:"data,omitempty" jsonschema:"title=Data"`
}

// Validate checks that the attachment looks okay.
func (a *Attachment) Validate() error {
return a.ValidateWithContext(context.Background())
}

// ValidateWithContext checks that the attachment looks okay within
// the provided context.
func (a *Attachment) ValidateWithContext(ctx context.Context) error {
return tax.ValidateStructWithContext(ctx, a,
validation.Field(&a.Key),
validation.Field(&a.Code),
validation.Field(&a.Name, validation.Required),
validation.Field(&a.Description),
validation.Field(&a.URL,
is.URL,
validation.When(
len(a.Data) == 0,
validation.Required,
),
),
validation.Field(&a.Data,
validation.When(
len(a.URL) > 0,
validation.Empty.Error("must be blank with url"),
),
),
validation.Field(&a.Digest),
validation.Field(&a.MIME,
// MIME types as defined by EN 16931-1:2017
validation.In(
"application/pdf",
"image/jpeg",
"image/png",
"test/csv",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"applicaiton/vnd.oasis.opendocument.spreadsheet",
),
),
)
}
43 changes: 43 additions & 0 deletions org/attachment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org_test

import (
"testing"

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

func TestAttachmentValidation(t *testing.T) {
t.Run("valid", func(t *testing.T) {
a := &org.Attachment{
Key: "key",
Code: "ABC",
Name: "test.txt",
URL: "https://example.com/test.txt",
}
err := a.Validate()
assert.NoError(t, err)
})

t.Run("both URL and data", func(t *testing.T) {
a := &org.Attachment{
Key: "key",
Code: "ABC",
Name: "test.txt",
URL: "https://example.com/test.txt",
Data: []byte("test"),
}
err := a.Validate()
assert.ErrorContains(t, err, "data: must be blank with url")
})

t.Run("missing URL and data", func(t *testing.T) {
a := &org.Attachment{
Key: "key",
Code: "ABC",
Name: "test.txt",
}
err := a.Validate()
assert.ErrorContains(t, err, "url: cannot be blank")
})
}
1 change: 1 addition & 0 deletions org/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ func init() {
Telephone{},
Unit(""),
Website{},
Attachment{},
)
}

0 comments on commit c3a06e2

Please sign in to comment.