diff --git a/api/addons.go b/api/addons.go index 8cadf98..d46ad14 100644 --- a/api/addons.go +++ b/api/addons.go @@ -1,13 +1,12 @@ package api import ( + "errors" "net/http" - "path" - "strings" "github.com/invopop/gobl" + "github.com/invopop/gobl.dev/internal/ops" "github.com/invopop/gobl/cbc" - "github.com/invopop/gobl/data" "github.com/invopop/gobl/i18n" "github.com/invopop/gobl/tax" ) @@ -40,14 +39,13 @@ func handleAddon(w http.ResponseWriter, r *http.Request) { return } - if !strings.HasSuffix(key, ".json") { - key = key + ".json" - } - p := path.Join("addons", key) - - d, err := data.Content.ReadFile(p) + d, err := ops.AddonData(key) if err != nil { - WriteError(w, gobl.ErrNotFound.WithReason("addon not found")) + if errors.Is(err, ops.ErrAddonNotFound) { + WriteError(w, gobl.ErrNotFound.WithReason("addon not found")) + return + } + WriteError(w, gobl.ErrInternal.WithCause(err)) return } WriteRawJSON(w, d) diff --git a/api/api_test.go b/api/api_test.go index d4db18c..7601c11 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -10,7 +10,14 @@ import ( "github.com/invopop/gobl" "github.com/invopop/gobl.dev/api" + + // Register the full GOBL addon set, as the real binaries do, so the + // addon endpoints are exercised against externally-implemented addons + // and not just those compiled into core GOBL. + _ "github.com/invopop/gobl.dev/bundle" + "github.com/invopop/gobl/dsig" + "github.com/invopop/gobl/tax" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -556,6 +563,43 @@ func TestAddonEndpoint(t *testing.T) { defer resp.Body.Close() //nolint:errcheck assert.Equal(t, http.StatusNotFound, resp.StatusCode) }) + + // Addons implemented in their own modules ship no file in core GOBL's + // embedded data directory, so serving them from there returned 404 even + // though /addons advertised them. + t.Run("externally implemented addon", func(t *testing.T) { + resp, err := http.Get(srv.URL + prefix + "/addons/mx-cfdi-v4") + require.NoError(t, err) + defer resp.Body.Close() //nolint:errcheck + require.Equal(t, http.StatusOK, resp.StatusCode) + + var def map[string]any + require.NoError(t, json.NewDecoder(resp.Body).Decode(&def)) + assert.Equal(t, "mx-cfdi-v4", def["key"]) + assert.Equal(t, "https://gobl.org/draft-0/tax/addon-def", def["$schema"]) + assert.NotEmpty(t, def["extensions"], "expected the full definition, not just a stub") + }) + + // The invariant that actually broke: anything /addons lists must be + // retrievable from /addons/{key}, whichever module implements it. + t.Run("every listed addon is retrievable", func(t *testing.T) { + defs := tax.AllAddonDefs() + require.NotEmpty(t, defs) + + for _, def := range defs { + key := def.Key.String() + t.Run(key, func(t *testing.T) { + resp, err := http.Get(srv.URL + prefix + "/addons/" + key) + require.NoError(t, err) + defer resp.Body.Close() //nolint:errcheck + require.Equal(t, http.StatusOK, resp.StatusCode) + + var got map[string]any + require.NoError(t, json.NewDecoder(resp.Body).Decode(&got)) + assert.Equal(t, key, got["key"]) + }) + } + }) } func TestRegimeListEndpoint(t *testing.T) { diff --git a/bundle/bundle.go b/bundle/bundle.go index 41e4e2e..6156599 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -12,6 +12,7 @@ import ( _ "github.com/invopop/gobl.fr.ctc/addon" _ "github.com/invopop/gobl.it.sdi/addon" _ "github.com/invopop/gobl.mx.cfdi/addon" + _ "github.com/invopop/gobl.pl.ksef/addon" _ "github.com/invopop/gobl.pt.saft/addon" _ "github.com/invopop/gobl.sa.zatca/addon" ) diff --git a/go.mod b/go.mod index 63c11a0..3621427 100644 --- a/go.mod +++ b/go.mod @@ -6,15 +6,16 @@ require ( github.com/a-h/templ v0.3.1001 github.com/google/go-cmp v0.7.0 github.com/imdario/mergo v0.3.16 - github.com/invopop/gobl v0.505.0 + github.com/invopop/gobl v0.507.0 github.com/invopop/gobl.br.nfe v0.0.4 - github.com/invopop/gobl.br.nfse v0.0.1 - github.com/invopop/gobl.dk.oioubl v0.0.1 + github.com/invopop/gobl.br.nfse v0.0.2 + github.com/invopop/gobl.dk.oioubl v0.0.6 github.com/invopop/gobl.fi.finvoice v0.0.2 github.com/invopop/gobl.fr.ctc v0.0.8 github.com/invopop/gobl.it.sdi v0.77.0 - github.com/invopop/gobl.mx.cfdi v0.63.0 - github.com/invopop/gobl.pt.saft v0.0.6 + github.com/invopop/gobl.mx.cfdi v0.64.0 + github.com/invopop/gobl.pl.ksef v0.45.0 + github.com/invopop/gobl.pt.saft v0.0.7 github.com/invopop/gobl.sa.zatca v0.0.4 github.com/invopop/icons v0.14.0 github.com/invopop/popui.go v0.30.0 @@ -28,7 +29,7 @@ require ( ) require ( - cloud.google.com/go v0.119.0 // indirect + cloud.google.com/go v0.121.3 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/Oudwins/tailwind-merge-go v0.2.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect diff --git a/go.sum b/go.sum index c184275..064eb97 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cloud.google.com/go v0.119.0 h1:tw7OjErMzJKbbjaEHkrt60KQrK5Wus/boCZ7tm5/RNE= -cloud.google.com/go v0.119.0/go.mod h1:fwB8QLzTcNevxqi8dcpR+hoMIs3jBherGS9VUBDAW08= +cloud.google.com/go v0.121.3 h1:84RD+hQXNdY5Sw/MWVAx5O9Aui/rd5VQ9HEcdN19afo= +cloud.google.com/go v0.121.3/go.mod h1:6vWF3nJWRrEUv26mMB3FEIU/o1MQNVPG1iHdisa2SJc= github.com/LastPossum/kamino v0.0.2 h1:Zry5lS7x7TTU1hzzk3Utnp+rX8kk/wWhuW52Ha9As+U= github.com/LastPossum/kamino v0.0.2/go.mod h1:H8Qm+6DGeNOoXk9hHIOEAQWS9nbo0YwK32pC/7REsOE= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= @@ -33,24 +33,26 @@ github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/invopop/gobl v0.505.0 h1:9ulIjXnoHt9bgJaOAVAoH08J9B9ETHIKPG097zbYlkc= -github.com/invopop/gobl v0.505.0/go.mod h1:HmiEdQreTSQYyNbhs81VKTmI7BAJKYC/6enh9RDwnE0= +github.com/invopop/gobl v0.507.0 h1:vg4Ohs8LAaDC/1YN9n4xOZrmQ83T7k98aUlw8HlG4nc= +github.com/invopop/gobl v0.507.0/go.mod h1:HmiEdQreTSQYyNbhs81VKTmI7BAJKYC/6enh9RDwnE0= github.com/invopop/gobl.br.nfe v0.0.4 h1:/RWQEwP8pHej+8u6u/BNeLKjYZ4NyZJRjWGF2cPEZdo= github.com/invopop/gobl.br.nfe v0.0.4/go.mod h1:qXzt4bz9S4+nJRbREb9QEAoO1W9r3Sjy6R207W7IuhY= -github.com/invopop/gobl.br.nfse v0.0.1 h1:BdwNiG7vk7bPMgE4m102P+UysOxmYWhOKIDfFI6RKEA= -github.com/invopop/gobl.br.nfse v0.0.1/go.mod h1:m22voo72ZScRiwIS49wXhzGRahzBO+IEAN0M0Kij3QI= -github.com/invopop/gobl.dk.oioubl v0.0.1 h1:A6owymml+RowMQdWnx2MV6usuMgmdOTl8n34f3MFFaQ= -github.com/invopop/gobl.dk.oioubl v0.0.1/go.mod h1:cVgy+dF9eaKd/qkfrRxr9pq2IyQWAWORk3Ol5rW9m0s= +github.com/invopop/gobl.br.nfse v0.0.2 h1:lhHnDBxs8sYBjWQ+nZ8q5/+6OUwRGPedgjJuxHDQdmI= +github.com/invopop/gobl.br.nfse v0.0.2/go.mod h1:rgq6Kw/yaRexA7yf9+sbAI4Ugj+DrlIKsWLxv8K2lAI= +github.com/invopop/gobl.dk.oioubl v0.0.6 h1:45Qg561DcB34Ldc8TDjvlP+3t7L43PqKxNXxphaWzqs= +github.com/invopop/gobl.dk.oioubl v0.0.6/go.mod h1:FFKB5iY3LtJZSsc+GqIs2vaptzInrVQjPEv9m2Ld2vQ= github.com/invopop/gobl.fi.finvoice v0.0.2 h1:yOPLBUVNZ8Uk3KEaYXjXTzqy4HV3xdyT4vkrp4ydtYM= github.com/invopop/gobl.fi.finvoice v0.0.2/go.mod h1:3NenHpVprZgry3em/42ddMBxTfngCCjpiKePAxoejbE= github.com/invopop/gobl.fr.ctc v0.0.8 h1:T4/QTD1qfHdACEeZSw6jMkK8U0eiwpaJXv1QZs6n6dc= github.com/invopop/gobl.fr.ctc v0.0.8/go.mod h1:S7D4Oz8cvKJ5usaY9Esjf6efx4gfDaC51/s0y9UVUF4= github.com/invopop/gobl.it.sdi v0.77.0 h1:v9IgNJrIDkb5aPZZJPkXcHmS3sMojCC7aafTCjn47V4= github.com/invopop/gobl.it.sdi v0.77.0/go.mod h1:+SfnlFTPjKSZZBrKEzVlrFePukufGV2AMSA5RFoWGEw= -github.com/invopop/gobl.mx.cfdi v0.63.0 h1:WkwsFCfMZfrG3N8ZHV+7NZ9VlCkYh4vRSUdqvrtEyHA= -github.com/invopop/gobl.mx.cfdi v0.63.0/go.mod h1:sXy7JIXauSKRqT0HgHsFICIV9YXuSHK3Quq9uZWZ9L0= -github.com/invopop/gobl.pt.saft v0.0.6 h1:K0pcFMq/Q5ojPJqdmRsYJKkzWcKWeyh9pPC5BkPUE14= -github.com/invopop/gobl.pt.saft v0.0.6/go.mod h1:z5L+dLFBx6PmEdacnFWehYWgiYZSM2HaPzx6ld+DnQs= +github.com/invopop/gobl.mx.cfdi v0.64.0 h1:JThygwAHB9TyR9seSrJepmfE77i51DLYDUKLOaJBZyU= +github.com/invopop/gobl.mx.cfdi v0.64.0/go.mod h1:Y0T5b3cruP4kx0IV/KHEhNVpm8O0FYbSCHXaBMWXKzQ= +github.com/invopop/gobl.pl.ksef v0.45.0 h1:XlK33Geb9aDG0G5PyCGa0Rp2JCcLDWq7JuiJEpMVEpA= +github.com/invopop/gobl.pl.ksef v0.45.0/go.mod h1:aAbp+2Z7kBosREe/kMzfW2Fm6Dud7E0LwvK7tFY1QF4= +github.com/invopop/gobl.pt.saft v0.0.7 h1:QCk+BA3nrWT6ikFLot2dmC/59Lj5U1BjPbHOCPy4RsU= +github.com/invopop/gobl.pt.saft v0.0.7/go.mod h1:jFAvKmLpSw566qvafNFbkvHdZz1ZjSjNgQvgcX5Gdxk= github.com/invopop/gobl.sa.zatca v0.0.4 h1:0BQKJdPazvdXCRosSU5pkQKCaexGQBATXEVqbAMajYk= github.com/invopop/gobl.sa.zatca v0.0.4/go.mod h1:tmLCoaKb4X7sZ2zRU8LalA1sgxM2kQv/uqMzZslhE6A= github.com/invopop/icons v0.14.0 h1:Bk0hF+EI/x3XwWX1IqMeFDMdu766zY3zFBqTkImCdhU= diff --git a/internal/mcp/resources.go b/internal/mcp/resources.go index 5aab642..c74d334 100644 --- a/internal/mcp/resources.go +++ b/internal/mcp/resources.go @@ -7,6 +7,7 @@ import ( "path" "strings" + "github.com/invopop/gobl.dev/internal/ops" "github.com/invopop/gobl/data" "github.com/invopop/gobl/schema" "github.com/mark3labs/mcp-go/mcp" @@ -129,12 +130,7 @@ func handleAddonResource(_ context.Context, request mcp.ReadResourceRequest) ([] return nil, fmt.Errorf("missing addon key") } - if !strings.HasSuffix(key, ".json") { - key = key + ".json" - } - p := path.Join("addons", key) - - d, err := data.Content.ReadFile(p) + d, err := ops.AddonData(key) if err != nil { return nil, fmt.Errorf("addon not found: %w", err) } @@ -190,12 +186,7 @@ func handleRegimeList(_ context.Context, _ mcp.ReadResourceRequest) ([]mcp.Resou } func handleAddonList(_ context.Context, _ mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { - items, err := listDataDir("addons") - if err != nil { - return nil, err - } - - d, err := marshalJSON(map[string]any{"addons": items}) + d, err := marshalJSON(map[string]any{"addons": ops.AddonKeys()}) if err != nil { return nil, err } diff --git a/internal/mcp/resources_test.go b/internal/mcp/resources_test.go index 77e34e6..278e7d0 100644 --- a/internal/mcp/resources_test.go +++ b/internal/mcp/resources_test.go @@ -5,6 +5,12 @@ import ( "encoding/json" "testing" + // Register the full GOBL addon set, as the real binaries do, so the addon + // resources are exercised against externally-implemented addons and not + // just those compiled into core GOBL. + _ "github.com/invopop/gobl.dev/bundle" + + "github.com/invopop/gobl/tax" "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -138,6 +144,30 @@ func TestHandleAddonResource(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "addon not found") }) + + // Addons implemented in their own modules ship no file in core GOBL's + // embedded data directory, so reading from there returned "addon not + // found" for every one of them. + t.Run("every registered addon is readable", func(t *testing.T) { + defs := tax.AllAddonDefs() + require.NotEmpty(t, defs) + + for _, def := range defs { + key := def.Key.String() + t.Run(key, func(t *testing.T) { + result, err := handleAddonResource(context.Background(), makeReadRequest("gobl://addons/"+key)) + require.NoError(t, err) + require.Len(t, result, 1) + + tc, ok := result[0].(mcp.TextResourceContents) + require.True(t, ok) + + var got map[string]any + require.NoError(t, json.Unmarshal([]byte(tc.Text), &got)) + assert.Equal(t, key, got["key"]) + }) + } + }) } func TestHandleSchemaList(t *testing.T) { @@ -192,6 +222,9 @@ func TestHandleAddonList(t *testing.T) { } require.NoError(t, json.Unmarshal([]byte(tc.Text), &data)) assert.NotEmpty(t, data.Addons) + assert.Contains(t, data.Addons, "es-verifactu-v1", "core addon missing") + assert.Contains(t, data.Addons, "mx-cfdi-v4", "externally-implemented addon missing") + assert.Len(t, data.Addons, len(tax.AllAddonDefs())) } func TestListDataDir(t *testing.T) { diff --git a/internal/mcp/tools.go b/internal/mcp/tools.go index 5e913dc..ad4f4ee 100644 --- a/internal/mcp/tools.go +++ b/internal/mcp/tools.go @@ -10,10 +10,10 @@ import ( "strings" "github.com/invopop/gobl" + "github.com/invopop/gobl.dev/internal/ops" "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/data" "github.com/invopop/gobl/i18n" - "github.com/invopop/gobl.dev/internal/ops" "github.com/invopop/gobl/pkg/here" "github.com/invopop/gobl/tax" "github.com/mark3labs/mcp-go/mcp" @@ -302,12 +302,7 @@ func handleAddonTool(_ context.Context, request mcp.CallToolRequest) (*mcp.CallT return mcp.NewToolResultError("'key' argument is required"), nil } - if !strings.HasSuffix(key, ".json") { - key = key + ".json" - } - p := path.Join("addons", key) - - d, err := data.Content.ReadFile(p) + d, err := ops.AddonData(key) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("addon not found: %s", err)), nil } diff --git a/internal/mcp/tools_test.go b/internal/mcp/tools_test.go index 30425af..4bc61e4 100644 --- a/internal/mcp/tools_test.go +++ b/internal/mcp/tools_test.go @@ -7,6 +7,12 @@ import ( "github.com/invopop/gobl" goblmcp "github.com/invopop/gobl.dev/internal/mcp" + + // Register the full GOBL addon set, as the real binaries do, so the addon + // tools are exercised against externally-implemented addons and not just + // those compiled into core GOBL. + _ "github.com/invopop/gobl.dev/bundle" + "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/stretchr/testify/assert" @@ -481,6 +487,20 @@ func TestAddonTool(t *testing.T) { text := extractText(t, result) assert.Contains(t, text, "key") }) + + // Addons implemented in their own modules ship no file in core GOBL's + // embedded data directory, so the tool reported them as not found. + t.Run("externally implemented addon", func(t *testing.T) { + result := callTool(t, s, "addon", map[string]any{ + "key": "mx-cfdi-v4", + }) + require.False(t, result.IsError) + + var got map[string]any + require.NoError(t, json.Unmarshal([]byte(extractText(t, result)), &got)) + assert.Equal(t, "mx-cfdi-v4", got["key"]) + assert.NotEmpty(t, got["extensions"], "expected the full definition, not just a stub") + }) } func TestAddonListTool(t *testing.T) { diff --git a/internal/ops/addons.go b/internal/ops/addons.go new file mode 100644 index 0000000..9c14924 --- /dev/null +++ b/internal/ops/addons.go @@ -0,0 +1,53 @@ +package ops + +import ( + "encoding/json" + "errors" + "strings" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/schema" + "github.com/invopop/gobl/tax" +) + +// ErrAddonNotFound is returned by [AddonData] when no addon is registered +// under the requested key. +var ErrAddonNotFound = errors.New("addon not found") + +// AddonData renders the full definition of the addon registered under key as +// indented JSON, wrapped in a schema object. +// +// The definition comes from the in-memory registry rather than GOBL's embedded +// data directory. Addons implemented in their own modules (gobl.mx.cfdi, +// gobl.it.sdi, and so on) register themselves via init() when blank-imported by +// the bundle, but ship no file inside core GOBL's data/addons — reading from +// there would serve only the addons still compiled into core. The output +// matches core GOBL's generated files byte for byte, since this mirrors what +// its addons/generate.go does. +// +// A ".json" suffix on key is accepted and ignored. +func AddonData(key string) ([]byte, error) { + key = strings.TrimSuffix(key, ".json") + + def := tax.AddonForKey(cbc.Key(key)) + if def == nil { + return nil, ErrAddonNotFound + } + + obj, err := schema.NewObject(def) + if err != nil { + return nil, err + } + return json.MarshalIndent(obj, "", " ") +} + +// AddonKeys returns the key of every registered addon, ordered as the registry +// reports them. +func AddonKeys() []string { + defs := tax.AllAddonDefs() + keys := make([]string, len(defs)) + for i, def := range defs { + keys[i] = string(def.Key) + } + return keys +} diff --git a/internal/ops/addons_test.go b/internal/ops/addons_test.go new file mode 100644 index 0000000..0348430 --- /dev/null +++ b/internal/ops/addons_test.go @@ -0,0 +1,87 @@ +package ops_test + +import ( + "encoding/json" + "testing" + + _ "github.com/invopop/gobl" + "github.com/invopop/gobl.dev/internal/ops" + + // Register the full GOBL addon set so externally-implemented addons are + // covered, not just those compiled into core GOBL. + _ "github.com/invopop/gobl.dev/bundle" + + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAddonData(t *testing.T) { + t.Run("core addon", func(t *testing.T) { + d, err := ops.AddonData("es-verifactu-v1") + require.NoError(t, err) + + var got map[string]any + require.NoError(t, json.Unmarshal(d, &got)) + assert.Equal(t, "es-verifactu-v1", got["key"]) + assert.Equal(t, "https://gobl.org/draft-0/tax/addon-def", got["$schema"]) + }) + + // Addons living in their own modules ship no file in core GOBL's embedded + // data directory, so they have to be rendered from the registry. + t.Run("externally implemented addon", func(t *testing.T) { + d, err := ops.AddonData("mx-cfdi-v4") + require.NoError(t, err) + + var got map[string]any + require.NoError(t, json.Unmarshal(d, &got)) + assert.Equal(t, "mx-cfdi-v4", got["key"]) + assert.NotEmpty(t, got["extensions"], "expected the full definition, not just a stub") + }) + + t.Run("json suffix accepted", func(t *testing.T) { + with, err := ops.AddonData("mx-cfdi-v4.json") + require.NoError(t, err) + without, err := ops.AddonData("mx-cfdi-v4") + require.NoError(t, err) + assert.Equal(t, without, with) + }) + + t.Run("unknown key", func(t *testing.T) { + _, err := ops.AddonData("nonexistent-addon") + assert.ErrorIs(t, err, ops.ErrAddonNotFound) + }) + + t.Run("empty key", func(t *testing.T) { + _, err := ops.AddonData("") + assert.ErrorIs(t, err, ops.ErrAddonNotFound) + }) + + // Guards the invariant that broke: every registered addon must render, + // whichever module implements it. + t.Run("every registered addon renders", func(t *testing.T) { + defs := tax.AllAddonDefs() + require.NotEmpty(t, defs) + + for _, def := range defs { + key := def.Key.String() + t.Run(key, func(t *testing.T) { + d, err := ops.AddonData(key) + require.NoError(t, err) + + var got map[string]any + require.NoError(t, json.Unmarshal(d, &got)) + assert.Equal(t, key, got["key"]) + }) + } + }) +} + +func TestAddonKeys(t *testing.T) { + keys := ops.AddonKeys() + require.NotEmpty(t, keys) + + assert.Len(t, keys, len(tax.AllAddonDefs())) + assert.Contains(t, keys, "es-verifactu-v1", "core addon missing") + assert.Contains(t, keys, "mx-cfdi-v4", "externally-implemented addon missing") +} diff --git a/internal/ops/bulk_test.go b/internal/ops/bulk_test.go index 50ec28f..10f918b 100644 --- a/internal/ops/bulk_test.go +++ b/internal/ops/bulk_test.go @@ -618,7 +618,7 @@ func TestBulk(t *testing.T) { //nolint:gocyclo // Following raw message is copied and pasted! (sorry!) Payload: json.RawMessage(`{ "list": [ - "https://gobl.org/draft-0/bill/action", "https://gobl.org/draft-0/bill/charge", "https://gobl.org/draft-0/bill/correction-options", "https://gobl.org/draft-0/bill/delivery", "https://gobl.org/draft-0/bill/delivery-details", "https://gobl.org/draft-0/bill/discount", "https://gobl.org/draft-0/bill/fault", "https://gobl.org/draft-0/bill/invoice", "https://gobl.org/draft-0/bill/line", "https://gobl.org/draft-0/bill/order", "https://gobl.org/draft-0/bill/ordering", "https://gobl.org/draft-0/bill/payment", "https://gobl.org/draft-0/bill/payment-details", "https://gobl.org/draft-0/bill/reason", "https://gobl.org/draft-0/bill/status", "https://gobl.org/draft-0/bill/status-line", "https://gobl.org/draft-0/bill/tax", "https://gobl.org/draft-0/bill/totals", "https://gobl.org/draft-0/cal/date", "https://gobl.org/draft-0/cal/date-time", "https://gobl.org/draft-0/cal/period", "https://gobl.org/draft-0/cal/time", "https://gobl.org/draft-0/cal/timestamp", "https://gobl.org/draft-0/cbc/code", "https://gobl.org/draft-0/cbc/code-map", "https://gobl.org/draft-0/cbc/definition", "https://gobl.org/draft-0/cbc/key", "https://gobl.org/draft-0/cbc/meta", "https://gobl.org/draft-0/cbc/source", "https://gobl.org/draft-0/cbc/uri", "https://gobl.org/draft-0/currency/amount", "https://gobl.org/draft-0/currency/code", "https://gobl.org/draft-0/currency/exchange-rate", "https://gobl.org/draft-0/dsig/digest", "https://gobl.org/draft-0/dsig/signature", "https://gobl.org/draft-0/envelope", "https://gobl.org/draft-0/head/header", "https://gobl.org/draft-0/head/link", "https://gobl.org/draft-0/head/stamp", "https://gobl.org/draft-0/i18n/string", "https://gobl.org/draft-0/l10n/code", "https://gobl.org/draft-0/l10n/iso-country-code", "https://gobl.org/draft-0/l10n/tax-country-code", "https://gobl.org/draft-0/note/message", "https://gobl.org/draft-0/num/amount", "https://gobl.org/draft-0/num/percentage", "https://gobl.org/draft-0/org/address", "https://gobl.org/draft-0/org/attachment", "https://gobl.org/draft-0/org/attribute", "https://gobl.org/draft-0/org/coordinates", "https://gobl.org/draft-0/org/document-ref", "https://gobl.org/draft-0/org/email", "https://gobl.org/draft-0/org/endpoint", "https://gobl.org/draft-0/org/identity", "https://gobl.org/draft-0/org/image", "https://gobl.org/draft-0/org/inbox", "https://gobl.org/draft-0/org/item", "https://gobl.org/draft-0/org/name", "https://gobl.org/draft-0/org/note", "https://gobl.org/draft-0/org/party", "https://gobl.org/draft-0/org/person", "https://gobl.org/draft-0/org/registration", "https://gobl.org/draft-0/org/telephone", "https://gobl.org/draft-0/org/unit", "https://gobl.org/draft-0/org/website", "https://gobl.org/draft-0/pay/card", "https://gobl.org/draft-0/pay/credit-transfer", "https://gobl.org/draft-0/pay/direct-debit", "https://gobl.org/draft-0/pay/instructions", "https://gobl.org/draft-0/pay/online", "https://gobl.org/draft-0/pay/record", "https://gobl.org/draft-0/pay/terms", "https://gobl.org/draft-0/regimes/mx/food-vouchers", "https://gobl.org/draft-0/regimes/mx/fuel-account-balance", "https://gobl.org/draft-0/schema/object", "https://gobl.org/draft-0/tax/addon-def", "https://gobl.org/draft-0/tax/addon-list", "https://gobl.org/draft-0/tax/catalogue-def", "https://gobl.org/draft-0/tax/correction-definition", "https://gobl.org/draft-0/tax/correction-set", "https://gobl.org/draft-0/tax/extensions", "https://gobl.org/draft-0/tax/identity", "https://gobl.org/draft-0/tax/note", "https://gobl.org/draft-0/tax/regime-code", "https://gobl.org/draft-0/tax/regime-def", "https://gobl.org/draft-0/tax/scenario", "https://gobl.org/draft-0/tax/scenario-set", "https://gobl.org/draft-0/tax/set", "https://gobl.org/draft-0/tax/tag-set", "https://gobl.org/draft-0/tax/total" + "https://gobl.org/draft-0/bill/action", "https://gobl.org/draft-0/bill/charge", "https://gobl.org/draft-0/bill/correction-options", "https://gobl.org/draft-0/bill/delivery", "https://gobl.org/draft-0/bill/delivery-details", "https://gobl.org/draft-0/bill/discount", "https://gobl.org/draft-0/bill/fault", "https://gobl.org/draft-0/bill/invoice", "https://gobl.org/draft-0/bill/line", "https://gobl.org/draft-0/bill/order", "https://gobl.org/draft-0/bill/ordering", "https://gobl.org/draft-0/bill/payment", "https://gobl.org/draft-0/bill/payment-details", "https://gobl.org/draft-0/bill/reason", "https://gobl.org/draft-0/bill/status", "https://gobl.org/draft-0/bill/status-line", "https://gobl.org/draft-0/bill/tax", "https://gobl.org/draft-0/bill/totals", "https://gobl.org/draft-0/cal/date", "https://gobl.org/draft-0/cal/date-time", "https://gobl.org/draft-0/cal/period", "https://gobl.org/draft-0/cal/time", "https://gobl.org/draft-0/cal/timestamp", "https://gobl.org/draft-0/cbc/code", "https://gobl.org/draft-0/cbc/code-map", "https://gobl.org/draft-0/cbc/definition", "https://gobl.org/draft-0/cbc/key", "https://gobl.org/draft-0/cbc/meta", "https://gobl.org/draft-0/cbc/source", "https://gobl.org/draft-0/cbc/uri", "https://gobl.org/draft-0/currency/amount", "https://gobl.org/draft-0/currency/code", "https://gobl.org/draft-0/currency/exchange-rate", "https://gobl.org/draft-0/dsig/digest", "https://gobl.org/draft-0/dsig/signature", "https://gobl.org/draft-0/envelope", "https://gobl.org/draft-0/head/header", "https://gobl.org/draft-0/head/link", "https://gobl.org/draft-0/head/stamp", "https://gobl.org/draft-0/i18n/string", "https://gobl.org/draft-0/l10n/code", "https://gobl.org/draft-0/l10n/iso-country-code", "https://gobl.org/draft-0/l10n/tax-country-code", "https://gobl.org/draft-0/note/message", "https://gobl.org/draft-0/num/amount", "https://gobl.org/draft-0/num/percentage", "https://gobl.org/draft-0/org/address", "https://gobl.org/draft-0/org/attachment", "https://gobl.org/draft-0/org/attribute", "https://gobl.org/draft-0/org/coordinates", "https://gobl.org/draft-0/org/document-ref", "https://gobl.org/draft-0/org/email", "https://gobl.org/draft-0/org/endpoint", "https://gobl.org/draft-0/org/identity", "https://gobl.org/draft-0/org/image", "https://gobl.org/draft-0/org/inbox", "https://gobl.org/draft-0/org/item", "https://gobl.org/draft-0/org/name", "https://gobl.org/draft-0/org/note", "https://gobl.org/draft-0/org/party", "https://gobl.org/draft-0/org/person", "https://gobl.org/draft-0/org/registration", "https://gobl.org/draft-0/org/telephone", "https://gobl.org/draft-0/org/website", "https://gobl.org/draft-0/pay/card", "https://gobl.org/draft-0/pay/credit-transfer", "https://gobl.org/draft-0/pay/direct-debit", "https://gobl.org/draft-0/pay/instructions", "https://gobl.org/draft-0/pay/online", "https://gobl.org/draft-0/pay/record", "https://gobl.org/draft-0/pay/terms", "https://gobl.org/draft-0/regimes/mx/food-vouchers", "https://gobl.org/draft-0/regimes/mx/fuel-account-balance", "https://gobl.org/draft-0/schema/object", "https://gobl.org/draft-0/tax/addon-def", "https://gobl.org/draft-0/tax/addon-list", "https://gobl.org/draft-0/tax/catalogue-def", "https://gobl.org/draft-0/tax/correction-definition", "https://gobl.org/draft-0/tax/correction-set", "https://gobl.org/draft-0/tax/extensions", "https://gobl.org/draft-0/tax/identity", "https://gobl.org/draft-0/tax/note", "https://gobl.org/draft-0/tax/regime-code", "https://gobl.org/draft-0/tax/regime-def", "https://gobl.org/draft-0/tax/scenario", "https://gobl.org/draft-0/tax/scenario-set", "https://gobl.org/draft-0/tax/set", "https://gobl.org/draft-0/tax/tag-set", "https://gobl.org/draft-0/tax/total" ] }`), IsFinal: false,