feat: user defined currencies (#700)

* basic currency service for loading at runtime

* api endpoint for currencies

* sort slice before return

* remove currency validation

* validate using currency service

* implement selecting dynamic currency options

* bump go version

* fix type definition

* specify explicit type

* change go versions

* proper types for assetId

* log/return currency error

* make case insensative

* use ToUpper instead

* feat: adding new currencies (#715)

* fix: task swag (#710)

Co-authored-by: Quoing <pavel.cadersky@mavenir.com>

* [feat] Adding new currencies

---------

Co-authored-by: quoing <quoing@users.noreply.github.com>
Co-authored-by: Quoing <pavel.cadersky@mavenir.com>
Co-authored-by: Bradley <41597815+userbradley@users.noreply.github.com>

* remove ts file and consoldate new values into json

* move flag to options namespace

* add env config for currencies

* basic documentaion

* remove in sync test

---------

Co-authored-by: quoing <quoing@users.noreply.github.com>
Co-authored-by: Quoing <pavel.cadersky@mavenir.com>
Co-authored-by: Bradley <41597815+userbradley@users.noreply.github.com>
Former-commit-id: c4b923847a
This commit is contained in:
Hayden 2024-01-18 13:45:42 -06:00 committed by GitHub
parent ce923a5b4c
commit 2b79788fbe
39 changed files with 1226 additions and 328 deletions

View File

@ -35,6 +35,6 @@
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node", "remoteUser": "node",
"features": { "features": {
"golang": "1.20" "golang": "1.21"
} }
} }

View File

@ -12,7 +12,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.20" go-version: "1.21"
- name: Install Task - name: Install Task
uses: arduino/setup-task@v1 uses: arduino/setup-task@v1

View File

@ -46,7 +46,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.20" go-version: "1.21"
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:

View File

@ -127,6 +127,22 @@ func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) errchain.Hand
} }
} }
// HandleCurrency godoc
//
// @Summary Currency
// @Tags Base
// @Produce json
// @Success 200 {object} currencies.Currency
// @Router /v1/currency [GET]
func (ctrl *V1Controller) HandleCurrency() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
// Set Cache for 10 Minutes
w.Header().Set("Cache-Control", "max-age=600")
return server.JSON(w, http.StatusOK, ctrl.svc.Currencies.Slice())
}
}
func (ctrl *V1Controller) HandleCacheWS() errchain.HandlerFunc { func (ctrl *V1Controller) HandleCacheWS() errchain.HandlerFunc {
m := melody.New() m := melody.New()

View File

@ -6,6 +6,7 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/internal/web/adapters" "github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/httpkit/errchain"
) )
@ -52,6 +53,14 @@ func (ctrl *V1Controller) HandleGroupGet() errchain.HandlerFunc {
func (ctrl *V1Controller) HandleGroupUpdate() errchain.HandlerFunc { func (ctrl *V1Controller) HandleGroupUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, body repo.GroupUpdate) (repo.Group, error) { fn := func(r *http.Request, body repo.GroupUpdate) (repo.Group, error) {
auth := services.NewContext(r.Context()) auth := services.NewContext(r.Context())
ok := ctrl.svc.Currencies.IsSupported(body.Currency)
if !ok {
return repo.Group{}, validate.NewFieldErrors(
validate.NewFieldError("currency", "currency '" + body.Currency + "' is not supported"),
)
}
return ctrl.svc.Group.UpdateGroup(auth, body) return ctrl.svc.Group.UpdateGroup(auth, body)
} }

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
@ -13,6 +14,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/hay-kot/homebox/backend/internal/core/currencies"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
@ -126,12 +128,40 @@ func run(cfg *config.Config) error {
return err return err
} }
collectFuncs := []currencies.CollectorFunc{
currencies.CollectDefaults(),
}
if cfg.Options.CurrencyConfig != "" {
log.Info().
Str("path", cfg.Options.CurrencyConfig).
Msg("loading currency config file")
content, err := os.ReadFile(cfg.Options.CurrencyConfig)
if err != nil {
log.Fatal().
Err(err).
Str("path", cfg.Options.CurrencyConfig).
Msg("failed to read currency config file")
}
collectFuncs = append(collectFuncs, currencies.CollectJSON(bytes.NewReader(content)))
}
currencies, err := currencies.CollectionCurrencies(collectFuncs...)
if err != nil {
log.Fatal().
Err(err).
Msg("failed to collect currencies")
}
app.bus = eventbus.New() app.bus = eventbus.New()
app.db = c app.db = c
app.repos = repo.New(c, app.bus, cfg.Storage.Data) app.repos = repo.New(c, app.bus, cfg.Storage.Data)
app.services = services.New( app.services = services.New(
app.repos, app.repos,
services.WithAutoIncrementAssetID(cfg.Options.AutoIncrementAssetID), services.WithAutoIncrementAssetID(cfg.Options.AutoIncrementAssetID),
services.WithCurrencies(currencies),
) )
// ========================================================================= // =========================================================================

View File

@ -64,6 +64,8 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
BuildTime: buildTime, BuildTime: buildTime,
}))) })))
r.Get(v1Base("/currencies"), chain.ToHandlerFunc(v1Ctrl.HandleCurrency()))
providers := []v1.AuthProvider{ providers := []v1.AuthProvider{
providers.NewLocalProvider(a.services.User), providers.NewLocalProvider(a.services.User),
} }

View File

@ -150,6 +150,25 @@ const docTemplate = `{
} }
} }
}, },
"/v1/currency": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Base"
],
"summary": "Currency",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/currencies.Currency"
}
}
}
}
},
"/v1/groups": { "/v1/groups": {
"get": { "get": {
"security": [ "security": [
@ -410,6 +429,16 @@ const docTemplate = `{
"description": "location Ids", "description": "location Ids",
"name": "locations", "name": "locations",
"in": "query" "in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "parent Ids",
"name": "parentIds",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -1574,7 +1603,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/v1.ApiSummary" "$ref": "#/definitions/v1.APISummary"
} }
} }
} }
@ -1645,6 +1674,12 @@ const docTemplate = `{
"schema": { "schema": {
"$ref": "#/definitions/v1.LoginForm" "$ref": "#/definitions/v1.LoginForm"
} }
},
{
"type": "string",
"description": "auth provider",
"name": "provider",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -1823,6 +1858,23 @@ const docTemplate = `{
} }
}, },
"definitions": { "definitions": {
"currencies.Currency": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"local": {
"type": "string"
},
"name": {
"type": "string"
},
"symbol": {
"type": "string"
}
}
},
"repo.DocumentOut": { "repo.DocumentOut": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1998,12 +2050,6 @@ const docTemplate = `{
"$ref": "#/definitions/repo.ItemAttachment" "$ref": "#/definitions/repo.ItemAttachment"
} }
}, },
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemSummary"
}
},
"createdAt": { "createdAt": {
"type": "string" "type": "string"
}, },
@ -2181,8 +2227,7 @@ const docTemplate = `{
"type": "boolean" "type": "boolean"
}, },
"assetId": { "assetId": {
"type": "string", "type": "string"
"example": "0"
}, },
"description": { "description": {
"type": "string" "type": "string"
@ -2736,15 +2781,7 @@ const docTemplate = `{
} }
} }
}, },
"v1.ActionAmountResult": { "v1.APISummary": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.ApiSummary": {
"type": "object", "type": "object",
"properties": { "properties": {
"allowRegistration": { "allowRegistration": {
@ -2773,6 +2810,14 @@ const docTemplate = `{
} }
} }
}, },
"v1.ActionAmountResult": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.Build": { "v1.Build": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -143,6 +143,25 @@
} }
} }
}, },
"/v1/currency": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Base"
],
"summary": "Currency",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/currencies.Currency"
}
}
}
}
},
"/v1/groups": { "/v1/groups": {
"get": { "get": {
"security": [ "security": [
@ -403,6 +422,16 @@
"description": "location Ids", "description": "location Ids",
"name": "locations", "name": "locations",
"in": "query" "in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "parent Ids",
"name": "parentIds",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -1567,7 +1596,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/v1.ApiSummary" "$ref": "#/definitions/v1.APISummary"
} }
} }
} }
@ -1638,6 +1667,12 @@
"schema": { "schema": {
"$ref": "#/definitions/v1.LoginForm" "$ref": "#/definitions/v1.LoginForm"
} }
},
{
"type": "string",
"description": "auth provider",
"name": "provider",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -1816,6 +1851,23 @@
} }
}, },
"definitions": { "definitions": {
"currencies.Currency": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"local": {
"type": "string"
},
"name": {
"type": "string"
},
"symbol": {
"type": "string"
}
}
},
"repo.DocumentOut": { "repo.DocumentOut": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1991,12 +2043,6 @@
"$ref": "#/definitions/repo.ItemAttachment" "$ref": "#/definitions/repo.ItemAttachment"
} }
}, },
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemSummary"
}
},
"createdAt": { "createdAt": {
"type": "string" "type": "string"
}, },
@ -2174,8 +2220,7 @@
"type": "boolean" "type": "boolean"
}, },
"assetId": { "assetId": {
"type": "string", "type": "string"
"example": "0"
}, },
"description": { "description": {
"type": "string" "type": "string"
@ -2729,15 +2774,7 @@
} }
} }
}, },
"v1.ActionAmountResult": { "v1.APISummary": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.ApiSummary": {
"type": "object", "type": "object",
"properties": { "properties": {
"allowRegistration": { "allowRegistration": {
@ -2766,6 +2803,14 @@
} }
} }
}, },
"v1.ActionAmountResult": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.Build": { "v1.Build": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -1,5 +1,16 @@
basePath: /api basePath: /api
definitions: definitions:
currencies.Currency:
properties:
code:
type: string
local:
type: string
name:
type: string
symbol:
type: string
type: object
repo.DocumentOut: repo.DocumentOut:
properties: properties:
id: id:
@ -116,10 +127,6 @@ definitions:
items: items:
$ref: '#/definitions/repo.ItemAttachment' $ref: '#/definitions/repo.ItemAttachment'
type: array type: array
children:
items:
$ref: '#/definitions/repo.ItemSummary'
type: array
createdAt: createdAt:
type: string type: string
description: description:
@ -238,7 +245,6 @@ definitions:
archived: archived:
type: boolean type: boolean
assetId: assetId:
example: "0"
type: string type: string
description: description:
type: string type: string
@ -608,12 +614,7 @@ definitions:
token: token:
type: string type: string
type: object type: object
v1.ActionAmountResult: v1.APISummary:
properties:
completed:
type: integer
type: object
v1.ApiSummary:
properties: properties:
allowRegistration: allowRegistration:
type: boolean type: boolean
@ -632,6 +633,11 @@ definitions:
type: string type: string
type: array type: array
type: object type: object
v1.ActionAmountResult:
properties:
completed:
type: integer
type: object
v1.Build: v1.Build:
properties: properties:
buildTime: buildTime:
@ -789,6 +795,18 @@ paths:
summary: Get Item by Asset ID summary: Get Item by Asset ID
tags: tags:
- Items - Items
/v1/currency:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/currencies.Currency'
summary: Currency
tags:
- Base
/v1/groups: /v1/groups:
get: get:
produces: produces:
@ -942,6 +960,13 @@ paths:
type: string type: string
name: locations name: locations
type: array type: array
- collectionFormat: multi
description: parent Ids
in: query
items:
type: string
name: parentIds
type: array
produces: produces:
- application/json - application/json
responses: responses:
@ -1656,7 +1681,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/v1.ApiSummary' $ref: '#/definitions/v1.APISummary'
summary: Application Info summary: Application Info
tags: tags:
- Base - Base
@ -1699,6 +1724,10 @@ paths:
required: true required: true
schema: schema:
$ref: '#/definitions/v1.LoginForm' $ref: '#/definitions/v1.LoginForm'
- description: auth provider
in: query
name: provider
type: string
produces: produces:
- application/json - application/json
responses: responses:

View File

@ -115,6 +115,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
@ -122,6 +124,8 @@ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTS
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olahol/melody v1.1.4 h1:RQHfKZkQmDxI0+SLZRNBCn4LiXdqxLKRGSkT8Dyoe/E= github.com/olahol/melody v1.1.4 h1:RQHfKZkQmDxI0+SLZRNBCn4LiXdqxLKRGSkT8Dyoe/E=
github.com/olahol/melody v1.1.4/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4= github.com/olahol/melody v1.1.4/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
@ -139,6 +143,10 @@ github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

View File

@ -0,0 +1,99 @@
// Package currencies provides a shared definition of currencies. This uses a global
// variable to hold the currencies.
package currencies
import (
"bytes"
_ "embed"
"encoding/json"
"io"
"slices"
"strings"
"sync"
)
//go:embed currencies.json
var defaults []byte
type CollectorFunc func() ([]Currency, error)
func CollectJSON(reader io.Reader) CollectorFunc {
return func() ([]Currency, error) {
var currencies []Currency
err := json.NewDecoder(reader).Decode(&currencies)
if err != nil {
return nil, err
}
return currencies, nil
}
}
func CollectDefaults() CollectorFunc {
return CollectJSON(bytes.NewReader(defaults))
}
func CollectionCurrencies(collectors ...CollectorFunc) ([]Currency, error) {
out := make([]Currency, 0, len(collectors))
for i := range collectors {
c, err := collectors[i]()
if err != nil {
return nil, err
}
out = append(out, c...)
}
return out, nil
}
type Currency struct {
Name string `json:"name"`
Code string `json:"code"`
Local string `json:"local"`
Symbol string `json:"symbol"`
}
type CurrencyRegistry struct {
mu sync.RWMutex
registry map[string]Currency
}
func NewCurrencyService(currencies []Currency) *CurrencyRegistry {
registry := make(map[string]Currency, len(currencies))
for i := range currencies {
registry[currencies[i].Code] = currencies[i]
}
return &CurrencyRegistry{
registry: registry,
}
}
func (cs *CurrencyRegistry) Slice() []Currency {
cs.mu.RLock()
defer cs.mu.RUnlock()
keys := make([]string, 0, len(cs.registry))
for key := range cs.registry {
keys = append(keys, key)
}
slices.Sort(keys)
out := make([]Currency, 0, len(cs.registry))
for i := range keys {
out = append(out, cs.registry[keys[i]])
}
return out
}
func (cs *CurrencyRegistry) IsSupported(code string) bool {
upper := strings.ToUpper(code)
cs.mu.RLock()
defer cs.mu.RUnlock()
_, ok := cs.registry[upper]
return ok
}

View File

@ -0,0 +1,626 @@
[
{
"code": "AED",
"local": "United Arab Emirates",
"symbol": "د.إ",
"name": "United Arab Emirates Dirham"
},
{
"code": "AFN",
"local": "Afghanistan",
"symbol": "؋",
"name": "Afghan Afghani"
},
{
"code": "ALL",
"local": "Albania",
"symbol": "L",
"name": "Albanian Lek"
},
{
"code": "AMD",
"local": "Armenia",
"symbol": "֏",
"name": "Armenian Dram"
},
{
"code": "ANG",
"local": "Netherlands Antilles",
"symbol": "ƒ",
"name": "Netherlands Antillean Guilder"
},
{
"code": "AOA",
"local": "Angola",
"symbol": "Kz",
"name": "Angolan Kwanza"
},
{
"code": "ARS",
"local": "Argentina",
"symbol": "$",
"name": "Argentine Peso"
},
{
"code": "AUD",
"local": "Australia",
"symbol": "A$",
"name": "Australian Dollar"
},
{
"code": "AWG",
"local": "Aruba",
"symbol": "ƒ",
"name": "Aruban Florin"
},
{
"code": "AZN",
"local": "Azerbaijan",
"symbol": "₼",
"name": "Azerbaijani Manat"
},
{
"code": "BAM",
"local": "Bosnia and Herzegovina",
"symbol": "KM",
"name": "Bosnia and Herzegovina Convertible Mark"
},
{
"code": "BBD",
"local": "Barbados",
"symbol": "Bds$",
"name": "Barbadian Dollar"
},
{
"code": "BDT",
"local": "Bangladesh",
"symbol": "৳",
"name": "Bangladeshi Taka"
},
{
"code": "BGN",
"local": "Bulgaria",
"symbol": "лв",
"name": "Bulgarian lev"
},
{
"code": "BHD",
"local": "Bahrain",
"symbol": "ب.د",
"name": "Bahraini Dinar"
},
{
"code": "BIF",
"local": "Burundi",
"symbol": "FBu",
"name": "Burundian Franc"
},
{
"code": "BMD",
"local": "Bermuda",
"symbol": "BD$",
"name": "Bermudian Dollar"
},
{
"code": "BND",
"local": "Brunei",
"symbol": "B$",
"name": "Brunei Dollar"
},
{
"code": "BOB",
"local": "Bolivia",
"symbol": "Bs.",
"name": "Bolivian Boliviano"
},
{
"code": "BRL",
"local": "Brazil",
"symbol": "R$",
"name": "Brazilian Real"
},
{
"code": "BSD",
"local": "Bahamas",
"symbol": "B$",
"name": "Bahamian Dollar"
},
{
"code": "BTN",
"local": "Bhutan",
"symbol": "Nu.",
"name": "Bhutanese Ngultrum"
},
{
"code": "BWP",
"local": "Botswana",
"symbol": "P",
"name": "Botswana Pula"
},
{
"code": "BYN",
"local": "Belarus",
"symbol": "Br",
"name": "Belarusian Ruble"
},
{
"code": "BZD",
"local": "Belize",
"symbol": "BZ$",
"name": "Belize Dollar"
},
{
"code": "CAD",
"local": "Canada",
"symbol": "C$",
"name": "Canadian Dollar"
},
{
"code": "CDF",
"local": "Democratic Republic of the Congo",
"symbol": "FC",
"name": "Congolese Franc"
},
{
"code": "CHF",
"local": "Switzerland",
"symbol": "CHF",
"name": "Swiss Franc"
},
{
"code": "CLP",
"local": "Chile",
"symbol": "CL$",
"name": "Chilean Peso"
},
{
"code": "CNY",
"local": "China",
"symbol": "¥",
"name": "Chinese Yuan"
},
{
"code": "COP",
"local": "Colombia",
"symbol": "COL$",
"name": "Colombian Peso"
},
{
"code": "CRC",
"local": "Costa Rica",
"symbol": "₡",
"name": "Costa Rican Colón"
},
{
"code": "CUP",
"local": "Cuba",
"symbol": "₱",
"name": "Cuban Peso"
},
{
"code": "CVE",
"local": "Cape Verde",
"symbol": "$",
"name": "Cape Verdean Escudo"
},
{
"code": "CZK",
"local": "Czech Republic",
"symbol": "Kč",
"name": "Czech Koruna"
},
{
"code": "DJF",
"local": "Djibouti",
"symbol": "Fdj",
"name": "Djiboutian Franc"
},
{
"code": "DKK",
"local": "Denmark",
"symbol": "kr",
"name": "Danish Krone"
},
{
"code": "DOP",
"local": "Dominican Republic",
"symbol": "RD$",
"name": "Dominican Peso"
},
{
"code": "DZD",
"local": "Algeria",
"symbol": "د.ج",
"name": "Algerian Dinar"
},
{
"code": "EGP",
"local": "Egypt",
"symbol": "£",
"name": "Egyptian Pound"
},
{
"code": "ERN",
"local": "Eritrea",
"symbol": "Nfk",
"name": "Eritrean Nakfa"
},
{
"code": "ETB",
"local": "Ethiopia",
"symbol": "Br",
"name": "Ethiopian Birr"
},
{
"code": "EUR",
"local": "Eurozone",
"symbol": "€",
"name": "Euro"
},
{
"code": "FJD",
"local": "Fiji",
"symbol": "FJ$",
"name": "Fijian Dollar"
},
{
"code": "FKP",
"local": "Falkland Islands",
"symbol": "£",
"name": "Falkland Islands Pound"
},
{
"code": "FOK",
"local": "Faroe Islands",
"symbol": "kr",
"name": "Faroese Króna"
},
{
"code": "GBP",
"local": "United Kingdom",
"symbol": "£",
"name": "British Pound Sterling"
},
{
"code": "GEL",
"local": "Georgia",
"symbol": "₾",
"name": "Georgian Lari"
},
{
"code": "GGP",
"local": "Guernsey",
"symbol": "£",
"name": "Guernsey Pound"
},
{
"code": "GHS",
"local": "Ghana",
"symbol": "GH₵",
"name": "Ghanaian Cedi"
},
{
"code": "GIP",
"local": "Gibraltar",
"symbol": "£",
"name": "Gibraltar Pound"
},
{
"code": "GMD",
"local": "Gambia",
"symbol": "D",
"name": "Gambian Dalasi"
},
{
"code": "GNF",
"local": "Guinea",
"symbol": "FG",
"name": "Guinean Franc"
},
{
"code": "GTQ",
"local": "Guatemala",
"symbol": "Q",
"name": "Guatemalan Quetzal"
},
{
"code": "GYD",
"local": "Guyana",
"symbol": "GY$",
"name": "Guyanese Dollar"
},
{
"code": "HKD",
"local": "Hong Kong",
"symbol": "HK$",
"name": "Hong Kong Dollar"
},
{
"code": "HNL",
"local": "Honduras",
"symbol": "L",
"name": "Honduran Lempira"
},
{
"code": "HRK",
"local": "Croatia",
"symbol": "kn",
"name": "Croatian Kuna"
},
{
"code": "HTG",
"local": "Haiti",
"symbol": "G",
"name": "Haitian Gourde"
},
{
"code": "HUF",
"local": "Hungary",
"symbol": "Ft",
"name": "Hungarian Forint"
},
{
"code": "IDR",
"local": "Indonesia",
"symbol": "Rp",
"name": "Indonesian Rupiah"
},
{
"code": "ILS",
"local": "Israel",
"symbol": "₪",
"name": "Israeli New Shekel"
},
{
"code": "IMP",
"local": "Isle of Man",
"symbol": "£",
"name": "Manx Pound"
},
{
"code": "INR",
"local": "India",
"symbol": "₹",
"name": "Indian Rupee"
},
{
"code": "IQD",
"local": "Iraq",
"symbol": "ع.د",
"name": "Iraqi Dinar"
},
{
"code": "IRR",
"local": "Iran",
"symbol": "﷼",
"name": "Iranian Rial"
},
{
"code": "ISK",
"local": "Iceland",
"symbol": "kr",
"name": "Icelandic Króna"
},
{
"code": "JEP",
"local": "Jersey",
"symbol": "£",
"name": "Jersey Pound"
},
{
"code": "JMD",
"local": "Jamaica",
"symbol": "J$",
"name": "Jamaican Dollar"
},
{
"code": "JOD",
"local": "Jordan",
"symbol": "د.ا",
"name": "Jordanian Dinar"
},
{
"code": "JPY",
"local": "Japan",
"symbol": "¥",
"name": "Japanese Yen"
},
{
"code": "KES",
"local": "Kenya",
"symbol": "KSh",
"name": "Kenyan Shilling"
},
{
"code": "KGS",
"local": "Kyrgyzstan",
"symbol": "с",
"name": "Kyrgyzstani Som"
},
{
"code": "KHR",
"local": "Cambodia",
"symbol": "៛",
"name": "Cambodian Riel"
},
{
"code": "KID",
"local": "Kiribati",
"symbol": "$",
"name": "Kiribati Dollar"
},
{
"code": "KMF",
"local": "Comoros",
"symbol": "CF",
"name": "Comorian Franc"
},
{
"code": "KRW",
"local": "South Korea",
"symbol": "₩",
"name": "South Korean Won"
},
{
"code": "KWD",
"local": "Kuwait",
"symbol": "د.ك",
"name": "Kuwaiti Dinar"
},
{
"code": "KYD",
"local": "Cayman Islands",
"symbol": "CI$",
"name": "Cayman Islands Dollar"
},
{
"code": "KZT",
"local": "Kazakhstan",
"symbol": "₸",
"name": "Kazakhstani Tenge"
},
{
"code": "LAK",
"local": "Laos",
"symbol": "₭",
"name": "Lao Kip"
},
{
"code": "LBP",
"local": "Lebanon",
"symbol": "ل.ل",
"name": "Lebanese Pound"
},
{
"code": "LKR",
"local": "Sri Lanka",
"symbol": "₨",
"name": "Sri Lankan Rupee"
},
{
"code": "LRD",
"local": "Liberia",
"symbol": "L$",
"name": "Liberian Dollar"
},
{
"code": "LSL",
"local": "Lesotho",
"symbol": "M",
"name": "Lesotho Loti"
},
{
"code": "LYD",
"local": "Libya",
"symbol": "ل.د",
"name": "Libyan Dinar"
},
{
"code": "MAD",
"local": "Morocco",
"symbol": "د.م.",
"name": "Moroccan Dirham"
},
{
"code": "MDL",
"local": "Moldova",
"symbol": "lei",
"name": "Moldovan Leu"
},
{
"code": "MGA",
"local": "Madagascar",
"symbol": "Ar",
"name": "Malagasy Ariary"
},
{
"code": "MKD",
"local": "North Macedonia",
"symbol": "ден",
"name": "Macedonian Denar"
},
{
"code": "MMK",
"local": "Myanmar",
"symbol": "K",
"name": "Myanmar Kyat"
},
{
"code": "MNT",
"local": "Mongolia",
"symbol": "₮",
"name": "Mongolian Tugrik"
},
{
"code": "MOP",
"local": "Macau",
"symbol": "MOP$",
"name": "Macanese Pataca"
},
{
"code": "MRU",
"local": "Mauritania",
"symbol": "UM",
"name": "Mauritanian Ouguiya"
},
{
"code": "MUR",
"local": "Mauritius",
"symbol": "₨",
"name": "Mauritian Rupee"
},
{
"code": "MVR",
"local": "Maldives",
"symbol": "Rf",
"name": "Maldivian Rufiyaa"
},
{
"code": "MWK",
"local": "Malawi",
"symbol": "MK",
"name": "Malawian Kwacha"
},
{
"code": "MXN",
"local": "Mexico",
"symbol": "Mex$",
"name": "Mexican Peso"
},
{
"code": "MYR",
"local": "Malaysia",
"symbol": "RM",
"name": "Malaysian Ringgit"
},
{
"code": "MZN",
"local": "Mozambique",
"symbol": "MT",
"name": "Mozambican Metical"
},
{
"code": "NAD",
"local": "Namibia",
"symbol": "N$",
"name": "Namibian Dollar"
},
{
"code": "NGN",
"local": "Nigeria",
"symbol": "₦",
"name": "Nigerian Naira"
},
{
"code": "NIO",
"local": "Nicaragua",
"symbol": "C$",
"name": "Nicaraguan Córdoba"
},
{
"code": "UAH",
"local": "Ukraine",
"symbol": "₴",
"name": "Ukrainian Hryvnia"
}
]

View File

@ -2,6 +2,7 @@
package services package services
import ( import (
"github.com/hay-kot/homebox/backend/internal/core/currencies"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
) )
@ -10,12 +11,14 @@ type AllServices struct {
Group *GroupService Group *GroupService
Items *ItemService Items *ItemService
BackgroundService *BackgroundService BackgroundService *BackgroundService
Currencies *currencies.CurrencyRegistry
} }
type OptionsFunc func(*options) type OptionsFunc func(*options)
type options struct { type options struct {
autoIncrementAssetID bool autoIncrementAssetID bool
currencies []currencies.Currency
} }
func WithAutoIncrementAssetID(v bool) func(*options) { func WithAutoIncrementAssetID(v bool) func(*options) {
@ -24,13 +27,27 @@ func WithAutoIncrementAssetID(v bool) func(*options) {
} }
} }
func WithCurrencies(v []currencies.Currency) func(*options) {
return func(o *options) {
o.currencies = v
}
}
func New(repos *repo.AllRepos, opts ...OptionsFunc) *AllServices { func New(repos *repo.AllRepos, opts ...OptionsFunc) *AllServices {
if repos == nil { if repos == nil {
panic("repos cannot be nil") panic("repos cannot be nil")
} }
defaultCurrencies, err := currencies.CollectionCurrencies(
currencies.CollectDefaults(),
)
if err != nil {
panic("failed to collect default currencies")
}
options := &options{ options := &options{
autoIncrementAssetID: true, autoIncrementAssetID: true,
currencies: defaultCurrencies,
} }
for _, opt := range opts { for _, opt := range opts {
@ -45,5 +62,6 @@ func New(repos *repo.AllRepos, opts ...OptionsFunc) *AllServices {
autoIncrementAssetID: options.autoIncrementAssetID, autoIncrementAssetID: options.autoIncrementAssetID,
}, },
BackgroundService: &BackgroundService{repos}, BackgroundService: &BackgroundService{repos},
Currencies: currencies.NewCurrencyService(options.currencies),
} }
} }

View File

@ -6,6 +6,7 @@ import (
"os" "os"
"testing" "testing"
"github.com/hay-kot/homebox/backend/internal/core/currencies"
"github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
@ -61,7 +62,12 @@ func TestMain(m *testing.M) {
tClient = client tClient = client
tRepos = repo.New(tClient, tbus, os.TempDir()+"/homebox") tRepos = repo.New(tClient, tbus, os.TempDir()+"/homebox")
tSvc = New(tRepos)
defaults, _ := currencies.CollectionCurrencies(
currencies.CollectDefaults(),
)
tSvc = New(tRepos, WithCurrencies(defaults))
defer func() { _ = client.Close() }() defer func() { _ = client.Close() }()
bootstrap() bootstrap()

View File

@ -25,7 +25,7 @@ type Group struct {
// Name holds the value of the "name" field. // Name holds the value of the "name" field.
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
// Currency holds the value of the "currency" field. // Currency holds the value of the "currency" field.
Currency group.Currency `json:"currency,omitempty"` Currency string `json:"currency,omitempty"`
// Edges holds the relations/edges for other nodes in the graph. // Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the GroupQuery when eager-loading is set. // The values are being populated by the GroupQuery when eager-loading is set.
Edges GroupEdges `json:"edges"` Edges GroupEdges `json:"edges"`
@ -170,7 +170,7 @@ func (gr *Group) assignValues(columns []string, values []any) error {
if value, ok := values[i].(*sql.NullString); !ok { if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field currency", values[i]) return fmt.Errorf("unexpected type %T for field currency", values[i])
} else if value.Valid { } else if value.Valid {
gr.Currency = group.Currency(value.String) gr.Currency = value.String
} }
default: default:
gr.selectValues.Set(columns[i], values[i]) gr.selectValues.Set(columns[i], values[i])
@ -253,7 +253,7 @@ func (gr *Group) String() string {
builder.WriteString(gr.Name) builder.WriteString(gr.Name)
builder.WriteString(", ") builder.WriteString(", ")
builder.WriteString("currency=") builder.WriteString("currency=")
builder.WriteString(fmt.Sprintf("%v", gr.Currency)) builder.WriteString(gr.Currency)
builder.WriteByte(')') builder.WriteByte(')')
return builder.String() return builder.String()
} }

View File

@ -3,7 +3,6 @@
package group package group
import ( import (
"fmt"
"time" "time"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
@ -119,65 +118,12 @@ var (
UpdateDefaultUpdatedAt func() time.Time UpdateDefaultUpdatedAt func() time.Time
// NameValidator is a validator for the "name" field. It is called by the builders before save. // NameValidator is a validator for the "name" field. It is called by the builders before save.
NameValidator func(string) error NameValidator func(string) error
// DefaultCurrency holds the default value on creation for the "currency" field.
DefaultCurrency string
// DefaultID holds the default value on creation for the "id" field. // DefaultID holds the default value on creation for the "id" field.
DefaultID func() uuid.UUID DefaultID func() uuid.UUID
) )
// Currency defines the type for the "currency" enum field.
type Currency string
// CurrencyUsd is the default value of the Currency enum.
const DefaultCurrency = CurrencyUsd
// Currency values.
const (
CurrencyAed Currency = "aed"
CurrencyAud Currency = "aud"
CurrencyBgn Currency = "bgn"
CurrencyBrl Currency = "brl"
CurrencyCad Currency = "cad"
CurrencyChf Currency = "chf"
CurrencyCzk Currency = "czk"
CurrencyDkk Currency = "dkk"
CurrencyEur Currency = "eur"
CurrencyGbp Currency = "gbp"
CurrencyHkd Currency = "hkd"
CurrencyIdr Currency = "idr"
CurrencyInr Currency = "inr"
CurrencyJpy Currency = "jpy"
CurrencyKrw Currency = "krw"
CurrencyMxn Currency = "mxn"
CurrencyNok Currency = "nok"
CurrencyNzd Currency = "nzd"
CurrencyPln Currency = "pln"
CurrencyRmb Currency = "rmb"
CurrencyRon Currency = "ron"
CurrencyRub Currency = "rub"
CurrencySar Currency = "sar"
CurrencySek Currency = "sek"
CurrencySgd Currency = "sgd"
CurrencyThb Currency = "thb"
CurrencyTry Currency = "try"
CurrencyUsd Currency = "usd"
CurrencyXag Currency = "xag"
CurrencyXau Currency = "xau"
CurrencyZar Currency = "zar"
)
func (c Currency) String() string {
return string(c)
}
// CurrencyValidator is a validator for the "currency" field enum values. It is called by the builders before save.
func CurrencyValidator(c Currency) error {
switch c {
case CurrencyAed, CurrencyAud, CurrencyBgn, CurrencyBrl, CurrencyCad, CurrencyChf, CurrencyCzk, CurrencyDkk, CurrencyEur, CurrencyGbp, CurrencyHkd, CurrencyIdr, CurrencyInr, CurrencyJpy, CurrencyKrw, CurrencyMxn, CurrencyNok, CurrencyNzd, CurrencyPln, CurrencyRmb, CurrencyRon, CurrencyRub, CurrencySar, CurrencySek, CurrencySgd, CurrencyThb, CurrencyTry, CurrencyUsd, CurrencyXag, CurrencyXau, CurrencyZar:
return nil
default:
return fmt.Errorf("group: invalid enum value for currency field: %q", c)
}
}
// OrderOption defines the ordering options for the Group queries. // OrderOption defines the ordering options for the Group queries.
type OrderOption func(*sql.Selector) type OrderOption func(*sql.Selector)

View File

@ -71,6 +71,11 @@ func Name(v string) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldName, v)) return predicate.Group(sql.FieldEQ(FieldName, v))
} }
// Currency applies equality check predicate on the "currency" field. It's identical to CurrencyEQ.
func Currency(v string) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldCurrency, v))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field. // CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.Group { func CreatedAtEQ(v time.Time) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldCreatedAt, v)) return predicate.Group(sql.FieldEQ(FieldCreatedAt, v))
@ -217,25 +222,70 @@ func NameContainsFold(v string) predicate.Group {
} }
// CurrencyEQ applies the EQ predicate on the "currency" field. // CurrencyEQ applies the EQ predicate on the "currency" field.
func CurrencyEQ(v Currency) predicate.Group { func CurrencyEQ(v string) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldCurrency, v)) return predicate.Group(sql.FieldEQ(FieldCurrency, v))
} }
// CurrencyNEQ applies the NEQ predicate on the "currency" field. // CurrencyNEQ applies the NEQ predicate on the "currency" field.
func CurrencyNEQ(v Currency) predicate.Group { func CurrencyNEQ(v string) predicate.Group {
return predicate.Group(sql.FieldNEQ(FieldCurrency, v)) return predicate.Group(sql.FieldNEQ(FieldCurrency, v))
} }
// CurrencyIn applies the In predicate on the "currency" field. // CurrencyIn applies the In predicate on the "currency" field.
func CurrencyIn(vs ...Currency) predicate.Group { func CurrencyIn(vs ...string) predicate.Group {
return predicate.Group(sql.FieldIn(FieldCurrency, vs...)) return predicate.Group(sql.FieldIn(FieldCurrency, vs...))
} }
// CurrencyNotIn applies the NotIn predicate on the "currency" field. // CurrencyNotIn applies the NotIn predicate on the "currency" field.
func CurrencyNotIn(vs ...Currency) predicate.Group { func CurrencyNotIn(vs ...string) predicate.Group {
return predicate.Group(sql.FieldNotIn(FieldCurrency, vs...)) return predicate.Group(sql.FieldNotIn(FieldCurrency, vs...))
} }
// CurrencyGT applies the GT predicate on the "currency" field.
func CurrencyGT(v string) predicate.Group {
return predicate.Group(sql.FieldGT(FieldCurrency, v))
}
// CurrencyGTE applies the GTE predicate on the "currency" field.
func CurrencyGTE(v string) predicate.Group {
return predicate.Group(sql.FieldGTE(FieldCurrency, v))
}
// CurrencyLT applies the LT predicate on the "currency" field.
func CurrencyLT(v string) predicate.Group {
return predicate.Group(sql.FieldLT(FieldCurrency, v))
}
// CurrencyLTE applies the LTE predicate on the "currency" field.
func CurrencyLTE(v string) predicate.Group {
return predicate.Group(sql.FieldLTE(FieldCurrency, v))
}
// CurrencyContains applies the Contains predicate on the "currency" field.
func CurrencyContains(v string) predicate.Group {
return predicate.Group(sql.FieldContains(FieldCurrency, v))
}
// CurrencyHasPrefix applies the HasPrefix predicate on the "currency" field.
func CurrencyHasPrefix(v string) predicate.Group {
return predicate.Group(sql.FieldHasPrefix(FieldCurrency, v))
}
// CurrencyHasSuffix applies the HasSuffix predicate on the "currency" field.
func CurrencyHasSuffix(v string) predicate.Group {
return predicate.Group(sql.FieldHasSuffix(FieldCurrency, v))
}
// CurrencyEqualFold applies the EqualFold predicate on the "currency" field.
func CurrencyEqualFold(v string) predicate.Group {
return predicate.Group(sql.FieldEqualFold(FieldCurrency, v))
}
// CurrencyContainsFold applies the ContainsFold predicate on the "currency" field.
func CurrencyContainsFold(v string) predicate.Group {
return predicate.Group(sql.FieldContainsFold(FieldCurrency, v))
}
// HasUsers applies the HasEdge predicate on the "users" edge. // HasUsers applies the HasEdge predicate on the "users" edge.
func HasUsers() predicate.Group { func HasUsers() predicate.Group {
return predicate.Group(func(s *sql.Selector) { return predicate.Group(func(s *sql.Selector) {

View File

@ -63,15 +63,15 @@ func (gc *GroupCreate) SetName(s string) *GroupCreate {
} }
// SetCurrency sets the "currency" field. // SetCurrency sets the "currency" field.
func (gc *GroupCreate) SetCurrency(gr group.Currency) *GroupCreate { func (gc *GroupCreate) SetCurrency(s string) *GroupCreate {
gc.mutation.SetCurrency(gr) gc.mutation.SetCurrency(s)
return gc return gc
} }
// SetNillableCurrency sets the "currency" field if the given value is not nil. // SetNillableCurrency sets the "currency" field if the given value is not nil.
func (gc *GroupCreate) SetNillableCurrency(gr *group.Currency) *GroupCreate { func (gc *GroupCreate) SetNillableCurrency(s *string) *GroupCreate {
if gr != nil { if s != nil {
gc.SetCurrency(*gr) gc.SetCurrency(*s)
} }
return gc return gc
} }
@ -267,11 +267,6 @@ func (gc *GroupCreate) check() error {
if _, ok := gc.mutation.Currency(); !ok { if _, ok := gc.mutation.Currency(); !ok {
return &ValidationError{Name: "currency", err: errors.New(`ent: missing required field "Group.currency"`)} return &ValidationError{Name: "currency", err: errors.New(`ent: missing required field "Group.currency"`)}
} }
if v, ok := gc.mutation.Currency(); ok {
if err := group.CurrencyValidator(v); err != nil {
return &ValidationError{Name: "currency", err: fmt.Errorf(`ent: validator failed for field "Group.currency": %w`, err)}
}
}
return nil return nil
} }
@ -320,7 +315,7 @@ func (gc *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
_node.Name = value _node.Name = value
} }
if value, ok := gc.mutation.Currency(); ok { if value, ok := gc.mutation.Currency(); ok {
_spec.SetField(group.FieldCurrency, field.TypeEnum, value) _spec.SetField(group.FieldCurrency, field.TypeString, value)
_node.Currency = value _node.Currency = value
} }
if nodes := gc.mutation.UsersIDs(); len(nodes) > 0 { if nodes := gc.mutation.UsersIDs(); len(nodes) > 0 {

View File

@ -57,15 +57,15 @@ func (gu *GroupUpdate) SetNillableName(s *string) *GroupUpdate {
} }
// SetCurrency sets the "currency" field. // SetCurrency sets the "currency" field.
func (gu *GroupUpdate) SetCurrency(gr group.Currency) *GroupUpdate { func (gu *GroupUpdate) SetCurrency(s string) *GroupUpdate {
gu.mutation.SetCurrency(gr) gu.mutation.SetCurrency(s)
return gu return gu
} }
// SetNillableCurrency sets the "currency" field if the given value is not nil. // SetNillableCurrency sets the "currency" field if the given value is not nil.
func (gu *GroupUpdate) SetNillableCurrency(gr *group.Currency) *GroupUpdate { func (gu *GroupUpdate) SetNillableCurrency(s *string) *GroupUpdate {
if gr != nil { if s != nil {
gu.SetCurrency(*gr) gu.SetCurrency(*s)
} }
return gu return gu
} }
@ -370,11 +370,6 @@ func (gu *GroupUpdate) check() error {
return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Group.name": %w`, err)} return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Group.name": %w`, err)}
} }
} }
if v, ok := gu.mutation.Currency(); ok {
if err := group.CurrencyValidator(v); err != nil {
return &ValidationError{Name: "currency", err: fmt.Errorf(`ent: validator failed for field "Group.currency": %w`, err)}
}
}
return nil return nil
} }
@ -397,7 +392,7 @@ func (gu *GroupUpdate) sqlSave(ctx context.Context) (n int, err error) {
_spec.SetField(group.FieldName, field.TypeString, value) _spec.SetField(group.FieldName, field.TypeString, value)
} }
if value, ok := gu.mutation.Currency(); ok { if value, ok := gu.mutation.Currency(); ok {
_spec.SetField(group.FieldCurrency, field.TypeEnum, value) _spec.SetField(group.FieldCurrency, field.TypeString, value)
} }
if gu.mutation.UsersCleared() { if gu.mutation.UsersCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
@ -755,15 +750,15 @@ func (guo *GroupUpdateOne) SetNillableName(s *string) *GroupUpdateOne {
} }
// SetCurrency sets the "currency" field. // SetCurrency sets the "currency" field.
func (guo *GroupUpdateOne) SetCurrency(gr group.Currency) *GroupUpdateOne { func (guo *GroupUpdateOne) SetCurrency(s string) *GroupUpdateOne {
guo.mutation.SetCurrency(gr) guo.mutation.SetCurrency(s)
return guo return guo
} }
// SetNillableCurrency sets the "currency" field if the given value is not nil. // SetNillableCurrency sets the "currency" field if the given value is not nil.
func (guo *GroupUpdateOne) SetNillableCurrency(gr *group.Currency) *GroupUpdateOne { func (guo *GroupUpdateOne) SetNillableCurrency(s *string) *GroupUpdateOne {
if gr != nil { if s != nil {
guo.SetCurrency(*gr) guo.SetCurrency(*s)
} }
return guo return guo
} }
@ -1081,11 +1076,6 @@ func (guo *GroupUpdateOne) check() error {
return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Group.name": %w`, err)} return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Group.name": %w`, err)}
} }
} }
if v, ok := guo.mutation.Currency(); ok {
if err := group.CurrencyValidator(v); err != nil {
return &ValidationError{Name: "currency", err: fmt.Errorf(`ent: validator failed for field "Group.currency": %w`, err)}
}
}
return nil return nil
} }
@ -1125,7 +1115,7 @@ func (guo *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error
_spec.SetField(group.FieldName, field.TypeString, value) _spec.SetField(group.FieldName, field.TypeString, value)
} }
if value, ok := guo.mutation.Currency(); ok { if value, ok := guo.mutation.Currency(); ok {
_spec.SetField(group.FieldCurrency, field.TypeEnum, value) _spec.SetField(group.FieldCurrency, field.TypeString, value)
} }
if guo.mutation.UsersCleared() { if guo.mutation.UsersCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{

View File

@ -117,7 +117,7 @@ var (
{Name: "created_at", Type: field.TypeTime}, {Name: "created_at", Type: field.TypeTime},
{Name: "updated_at", Type: field.TypeTime}, {Name: "updated_at", Type: field.TypeTime},
{Name: "name", Type: field.TypeString, Size: 255}, {Name: "name", Type: field.TypeString, Size: 255},
{Name: "currency", Type: field.TypeEnum, Enums: []string{"aed", "aud", "bgn", "brl", "cad", "chf", "czk", "dkk", "eur", "gbp", "hkd", "idr", "inr", "jpy", "krw", "mxn", "nok", "nzd", "pln", "rmb", "ron", "rub", "sar", "sek", "sgd", "thb", "try", "usd", "xag", "xau", "zar"}, Default: "usd"}, {Name: "currency", Type: field.TypeString, Default: "usd"},
} }
// GroupsTable holds the schema information for the "groups" table. // GroupsTable holds the schema information for the "groups" table.
GroupsTable = &schema.Table{ GroupsTable = &schema.Table{

View File

@ -2340,7 +2340,7 @@ type GroupMutation struct {
created_at *time.Time created_at *time.Time
updated_at *time.Time updated_at *time.Time
name *string name *string
currency *group.Currency currency *string
clearedFields map[string]struct{} clearedFields map[string]struct{}
users map[uuid.UUID]struct{} users map[uuid.UUID]struct{}
removedusers map[uuid.UUID]struct{} removedusers map[uuid.UUID]struct{}
@ -2581,12 +2581,12 @@ func (m *GroupMutation) ResetName() {
} }
// SetCurrency sets the "currency" field. // SetCurrency sets the "currency" field.
func (m *GroupMutation) SetCurrency(gr group.Currency) { func (m *GroupMutation) SetCurrency(s string) {
m.currency = &gr m.currency = &s
} }
// Currency returns the value of the "currency" field in the mutation. // Currency returns the value of the "currency" field in the mutation.
func (m *GroupMutation) Currency() (r group.Currency, exists bool) { func (m *GroupMutation) Currency() (r string, exists bool) {
v := m.currency v := m.currency
if v == nil { if v == nil {
return return
@ -2597,7 +2597,7 @@ func (m *GroupMutation) Currency() (r group.Currency, exists bool) {
// OldCurrency returns the old "currency" field's value of the Group entity. // OldCurrency returns the old "currency" field's value of the Group entity.
// If the Group object wasn't provided to the builder, the object is fetched from the database. // If the Group object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails. // An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *GroupMutation) OldCurrency(ctx context.Context) (v group.Currency, err error) { func (m *GroupMutation) OldCurrency(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) { if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldCurrency is only allowed on UpdateOne operations") return v, errors.New("OldCurrency is only allowed on UpdateOne operations")
} }
@ -3105,7 +3105,7 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error {
m.SetName(v) m.SetName(v)
return nil return nil
case group.FieldCurrency: case group.FieldCurrency:
v, ok := value.(group.Currency) v, ok := value.(string)
if !ok { if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name) return fmt.Errorf("unexpected type %T for field %s", value, name)
} }

View File

@ -161,6 +161,10 @@ func init() {
return nil return nil
} }
}() }()
// groupDescCurrency is the schema descriptor for currency field.
groupDescCurrency := groupFields[1].Descriptor()
// group.DefaultCurrency holds the default value on creation for the currency field.
group.DefaultCurrency = groupDescCurrency.Default.(string)
// groupDescID is the schema descriptor for id field. // groupDescID is the schema descriptor for id field.
groupDescID := groupMixinFields0[0].Descriptor() groupDescID := groupMixinFields0[0].Descriptor()
// group.DefaultID holds the default value on creation for the id field. // group.DefaultID holds the default value on creation for the id field.

View File

@ -27,41 +27,8 @@ func (Group) Fields() []ent.Field {
field.String("name"). field.String("name").
MaxLen(255). MaxLen(255).
NotEmpty(), NotEmpty(),
field.Enum("currency"). field.String("currency").
Default("usd"). Default("usd"),
Values(
"aed",
"aud",
"bgn",
"brl",
"cad",
"chf",
"czk",
"dkk",
"eur",
"gbp",
"hkd",
"idr",
"inr",
"jpy",
"krw",
"mxn",
"nok",
"nzd",
"pln",
"rmb",
"ron",
"rub",
"sar",
"sek",
"sgd",
"thb",
"try",
"usd",
"xag",
"xau",
"zar",
),
} }
} }

View File

@ -78,7 +78,6 @@ func (g UserMixin) Fields() []ent.Field {
} }
return nil return nil
} }
func (g UserMixin) Edges() []ent.Edge { func (g UserMixin) Edges() []ent.Edge {

View File

@ -28,7 +28,7 @@ func NewGroupRepository(db *ent.Client) *GroupRepository {
Name: g.Name, Name: g.Name,
CreatedAt: g.CreatedAt, CreatedAt: g.CreatedAt,
UpdatedAt: g.UpdatedAt, UpdatedAt: g.UpdatedAt,
Currency: strings.ToUpper(g.Currency.String()), Currency: strings.ToUpper(g.Currency),
} }
} }
@ -265,11 +265,9 @@ func (r *GroupRepository) GroupCreate(ctx context.Context, name string) (Group,
} }
func (r *GroupRepository) GroupUpdate(ctx context.Context, ID uuid.UUID, data GroupUpdate) (Group, error) { func (r *GroupRepository) GroupUpdate(ctx context.Context, ID uuid.UUID, data GroupUpdate) (Group, error) {
currency := group.Currency(strings.ToLower(data.Currency))
entity, err := r.db.Group.UpdateOneID(ID). entity, err := r.db.Group.UpdateOneID(ID).
SetName(data.Name). SetName(data.Name).
SetCurrency(currency). SetCurrency(strings.ToLower(data.Currency)).
Save(ctx) Save(ctx)
return r.groupMapper.MapErr(entity, err) return r.groupMapper.MapErr(entity, err)

View File

@ -68,7 +68,7 @@ type (
ItemUpdate struct { ItemUpdate struct {
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable,x-omitempty"` ParentID uuid.UUID `json:"parentId" extensions:"x-nullable,x-omitempty"`
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
AssetID AssetID `json:"assetId"` AssetID AssetID `json:"assetId" swaggertype:"string"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Quantity int `json:"quantity"` Quantity int `json:"quantity"`

View File

@ -17,19 +17,20 @@ const (
type Config struct { type Config struct {
conf.Version conf.Version
Mode string `yaml:"mode" conf:"default:development"` // development or production Mode string `yaml:"mode" conf:"default:development"` // development or production
Web WebConfig `yaml:"web"` Web WebConfig `yaml:"web"`
Storage Storage `yaml:"storage"` Storage Storage `yaml:"storage"`
Log LoggerConf `yaml:"logger"` Log LoggerConf `yaml:"logger"`
Mailer MailerConf `yaml:"mailer"` Mailer MailerConf `yaml:"mailer"`
Demo bool `yaml:"demo"` Demo bool `yaml:"demo"`
Debug DebugConf `yaml:"debug"` Debug DebugConf `yaml:"debug"`
Options Options `yaml:"options"` Options Options `yaml:"options"`
} }
type Options struct { type Options struct {
AllowRegistration bool `yaml:"disable_registration" conf:"default:true"` AllowRegistration bool `yaml:"disable_registration" conf:"default:true"`
AutoIncrementAssetID bool `yaml:"auto_increment_asset_id" conf:"default:true"` AutoIncrementAssetID bool `yaml:"auto_increment_asset_id" conf:"default:true"`
CurrencyConfig string `yaml:"currencies"`
} }
type DebugConf struct { type DebugConf struct {

View File

@ -88,7 +88,7 @@ func (fe FieldErrors) Nil() bool {
return len(fe) == 0 return len(fe) == 0
} }
// Error implments the error interface. // Error implements the error interface.
func (fe FieldErrors) Error() string { func (fe FieldErrors) Error() string {
d, err := json.Marshal(fe) d, err := json.Marshal(fe)
if err != nil { if err != nil {
@ -101,6 +101,10 @@ func NewFieldErrors(errs ...FieldError) FieldErrors {
return errs return errs
} }
func NewFieldError(field, reason string) FieldError {
return FieldError{Field: field, Error: reason}
}
func IsFieldError(err error) bool { func IsFieldError(err error) bool {
v := FieldErrors{} v := FieldErrors{}
return errors.As(err, &v) return errors.As(err, &v)

View File

@ -143,6 +143,25 @@
} }
} }
}, },
"/v1/currency": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Base"
],
"summary": "Currency",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/currencies.Currency"
}
}
}
}
},
"/v1/groups": { "/v1/groups": {
"get": { "get": {
"security": [ "security": [
@ -403,6 +422,16 @@
"description": "location Ids", "description": "location Ids",
"name": "locations", "name": "locations",
"in": "query" "in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "parent Ids",
"name": "parentIds",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -1567,7 +1596,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/v1.ApiSummary" "$ref": "#/definitions/v1.APISummary"
} }
} }
} }
@ -1638,6 +1667,12 @@
"schema": { "schema": {
"$ref": "#/definitions/v1.LoginForm" "$ref": "#/definitions/v1.LoginForm"
} }
},
{
"type": "string",
"description": "auth provider",
"name": "provider",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -1816,6 +1851,23 @@
} }
}, },
"definitions": { "definitions": {
"currencies.Currency": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"local": {
"type": "string"
},
"name": {
"type": "string"
},
"symbol": {
"type": "string"
}
}
},
"repo.DocumentOut": { "repo.DocumentOut": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1991,12 +2043,6 @@
"$ref": "#/definitions/repo.ItemAttachment" "$ref": "#/definitions/repo.ItemAttachment"
} }
}, },
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemSummary"
}
},
"createdAt": { "createdAt": {
"type": "string" "type": "string"
}, },
@ -2174,8 +2220,7 @@
"type": "boolean" "type": "boolean"
}, },
"assetId": { "assetId": {
"type": "string", "type": "string"
"example": "0"
}, },
"description": { "description": {
"type": "string" "type": "string"
@ -2729,15 +2774,7 @@
} }
} }
}, },
"v1.ActionAmountResult": { "v1.APISummary": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.ApiSummary": {
"type": "object", "type": "object",
"properties": { "properties": {
"allowRegistration": { "allowRegistration": {
@ -2766,6 +2803,14 @@
} }
} }
}, },
"v1.ActionAmountResult": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.Build": { "v1.Build": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -61,6 +61,7 @@ volumes:
| HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this | | HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this |
| HBOX_OPTIONS_ALLOW_REGISTRATION | true | allow users to register themselves | | HBOX_OPTIONS_ALLOW_REGISTRATION | true | allow users to register themselves |
| HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID | true | auto increments the asset_id field for new items | | HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID | true | auto increments the asset_id field for new items |
| HBOX_OPTIONS_CURRENCY_CONFIG | | json configuration file containing additional currencie |
| HBOX_WEB_MAX_UPLOAD_SIZE | 10 | maximum file upload size supported in MB | | HBOX_WEB_MAX_UPLOAD_SIZE | 10 | maximum file upload size supported in MB |
| HBOX_WEB_READ_TIMEOUT | 10 | Read timeout of HTTP sever | | HBOX_WEB_READ_TIMEOUT | 10 | Read timeout of HTTP sever |
| HBOX_WEB_WRITE_TIMEOUT | 10 | Write timeout of HTTP server | | HBOX_WEB_WRITE_TIMEOUT | 10 | Write timeout of HTTP server |
@ -104,6 +105,7 @@ volumes:
--debug-port/$HBOX_DEBUG_PORT <string> (default: 4000) --debug-port/$HBOX_DEBUG_PORT <string> (default: 4000)
--options-allow-registration/$HBOX_OPTIONS_ALLOW_REGISTRATION <bool> (default: true) --options-allow-registration/$HBOX_OPTIONS_ALLOW_REGISTRATION <bool> (default: true)
--options-auto-increment-asset-id/$HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID <bool> (default: true) --options-auto-increment-asset-id/$HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID <bool> (default: true)
--options-currency-config/$HBOX_OPTIONS_CURRENCY_CONFIG <string>
--help/-h --help/-h
display this help message display this help message
``` ```

View File

@ -55,4 +55,26 @@ Homebox uses [shoutrrr](https://containrrr.dev/shoutrrr/0.7/) to send notificati
**Notifications are sent on the day the maintenance is scheduled at or around 8am.** **Notifications are sent on the day the maintenance is scheduled at or around 8am.**
As of `v0.9.0` we have limited support for complex scheduling of maintenance events. If you have requests for extended functionality, please open an issue on GitHub or reach out on Discord. We're still gauging the demand for this feature. As of `v0.9.0` we have limited support for complex scheduling of maintenance events. If you have requests for extended functionality, please open an issue on GitHub or reach out on Discord. We're still gauging the demand for this feature.
## Custom Currencies
:octicons-tag-24: v0.11.0
Homebox allows you to add additional currencies to your instance by specify a JSON file containing the currencies you want to add.
**Environment Variable:** `HBOX_OPTIONS_CURRENCY_CONFIG`
### Example
```json
[
{
"code": "AED",
"local": "United Arab Emirates",
"symbol": "د.إ",
"name": "United Arab Emirates Dirham"
},
]
```

View File

@ -2,7 +2,6 @@ import { faker } from "@faker-js/faker";
import { describe, test, expect } from "vitest"; import { describe, test, expect } from "vitest";
import { factories } from "../factories"; import { factories } from "../factories";
import { sharedUserClient } from "../test-utils"; import { sharedUserClient } from "../test-utils";
import { currencies } from "~~/lib/data/currency";
describe("first time user workflow (register, login, join group)", () => { describe("first time user workflow (register, login, join group)", () => {
test("user should be able to update group", async () => { test("user should be able to update group", async () => {
@ -29,20 +28,6 @@ describe("first time user workflow (register, login, join group)", () => {
expect(group.currency).toBe("USD"); expect(group.currency).toBe("USD");
}); });
test("currencies should be in sync with backend", async () => {
const { client } = await factories.client.singleUse();
for (const currency of currencies) {
const { response, data: group } = await client.group.update({
name: faker.person.firstName(),
currency: currency.code,
});
expect(response.status).toBe(200);
expect(group.currency).toBe(currency.code);
}
});
test("user should be able to join create join token and have user signup", async () => { test("user should be able to join create join token and have user signup", async () => {
const api = factories.client.public(); const api = factories.client.public();

View File

@ -9,7 +9,7 @@ import { sharedUserClient } from "../test-utils";
describe("user should be able to create an item and add an attachment", () => { describe("user should be able to create an item and add an attachment", () => {
let increment = 0; let increment = 0;
/** /**
* useLocatio sets up a location resource for testing, and returns a function * useLocation sets up a location resource for testing, and returns a function
* that can be used to delete the location from the backend server. * that can be used to delete the location from the backend server.
*/ */
async function useLocation(api: UserClient): Promise<[LocationOut, () => Promise<void>]> { async function useLocation(api: UserClient): Promise<[LocationOut, () => Promise<void>]> {

View File

@ -1,5 +1,11 @@
import { BaseAPI, route } from "../base"; import { BaseAPI, route } from "../base";
import { Group, GroupInvitation, GroupInvitationCreate, GroupUpdate } from "../types/data-contracts"; import {
CurrenciesCurrency,
Group,
GroupInvitation,
GroupInvitationCreate,
GroupUpdate,
} from "../types/data-contracts";
export class GroupApi extends BaseAPI { export class GroupApi extends BaseAPI {
createInvitation(data: GroupInvitationCreate) { createInvitation(data: GroupInvitationCreate) {
@ -21,4 +27,10 @@ export class GroupApi extends BaseAPI {
url: route("/groups"), url: route("/groups"),
}); });
} }
currencies() {
return this.http.get<CurrenciesCurrency[]>({
url: route("/currencies"),
});
}
} }

View File

@ -1,5 +1,5 @@
import { BaseAPI, route } from "./base"; import { BaseAPI, route } from "./base";
import { ApiSummary, LoginForm, TokenResponse, UserRegistration } from "./types/data-contracts"; import { APISummary, LoginForm, TokenResponse, UserRegistration } from "./types/data-contracts";
export type StatusResult = { export type StatusResult = {
health: boolean; health: boolean;
@ -10,7 +10,7 @@ export type StatusResult = {
export class PublicApi extends BaseAPI { export class PublicApi extends BaseAPI {
public status() { public status() {
return this.http.get<ApiSummary>({ url: route("/status") }); return this.http.get<APISummary>({ url: route("/status") });
} }
public login(username: string, password: string, stayLoggedIn = false) { public login(username: string, password: string, stayLoggedIn = false) {

View File

@ -10,6 +10,13 @@
* --------------------------------------------------------------- * ---------------------------------------------------------------
*/ */
export interface CurrenciesCurrency {
code: string;
local: string;
name: string;
symbol: string;
}
export interface DocumentOut { export interface DocumentOut {
id: string; id: string;
path: string; path: string;
@ -81,7 +88,6 @@ export interface ItemOut {
/** @example "0" */ /** @example "0" */
assetId: string; assetId: string;
attachments: ItemAttachment[]; attachments: ItemAttachment[];
children: ItemSummary[];
createdAt: Date | string; createdAt: Date | string;
description: string; description: string;
fields: ItemField[]; fields: ItemField[];
@ -141,7 +147,6 @@ export interface ItemSummary {
export interface ItemUpdate { export interface ItemUpdate {
archived: boolean; archived: boolean;
/** @example "0" */
assetId: string; assetId: string;
description: string; description: string;
fields: ItemField[]; fields: ItemField[];
@ -364,11 +369,7 @@ export interface UserRegistration {
token: string; token: string;
} }
export interface ActionAmountResult { export interface APISummary {
completed: number;
}
export interface ApiSummary {
allowRegistration: boolean; allowRegistration: boolean;
build: Build; build: Build;
demo: boolean; demo: boolean;
@ -378,6 +379,10 @@ export interface ApiSummary {
versions: string[]; versions: string[];
} }
export interface ActionAmountResult {
completed: number;
}
export interface Build { export interface Build {
buildTime: string; buildTime: string;
commit: string; commit: string;

View File

@ -1,73 +0,0 @@
export type Codes =
| "AED"
| "AUD"
| "BGN"
| "BRL"
| "CAD"
| "CHF"
| "CZK"
| "DKK"
| "EUR"
| "GBP"
| "HKD"
| "IDR"
| "INR"
| "JPY"
| "KRW"
| "MXN"
| "NOK"
| "NZD"
| "PLN"
| "RMB"
| "RUB"
| "RON"
| "SAR"
| "SEK"
| "SGD"
| "THB"
| "TRY"
| "USD"
| "XAG"
| "XAU"
| "ZAR";
export type Currency = {
code: Codes;
local: string;
symbol: string;
name: string;
};
export const currencies: Currency[] = [
{ code: "AED", local: "United Arab Emirates", symbol: "د.إ", name: "United Arab Emirates Dirham" },
{ code: "AUD", local: "Australia", symbol: "A$", name: "Australian Dollar" },
{ code: "BGN", local: "bg-BG", symbol: "lv", name: "Bulgarian lev" },
{ code: "BRL", local: "Brazil", symbol: "R$", name: "Brazilian Real" },
{ code: "CAD", local: "Canada", symbol: "C$", name: "Canadian Dollar" },
{ code: "CHF", local: "Switzerland", symbol: "CHF", name: "Swiss Franc" },
{ code: "CZK", local: "cs-CZ", symbol: "Kč", name: "Czech Koruna" },
{ code: "DKK", local: "da-DK", symbol: "kr", name: "Danish Krone" },
{ code: "EUR", local: "Eurozone", symbol: "€", name: "Euro" },
{ code: "GBP", local: "United Kingdom", symbol: "£", name: "British Pound Sterling" },
{ code: "HKD", local: "Hong Kong", symbol: "HK$", name: "Hong Kong Dollar" },
{ code: "IDR", local: "Indonesia", symbol: "Rp", name: "Indonesian Rupiah" },
{ code: "INR", local: "India", symbol: "₹", name: "Indian Rupee" },
{ code: "JPY", local: "Japan", symbol: "¥", name: "Japanese Yen" },
{ code: "KRW", local: "South Korea", symbol: "₩", name: "South Korean Won" },
{ code: "MXN", local: "Mexico", symbol: "Mex$", name: "Mexican Peso" },
{ code: "NOK", local: "Norway", symbol: "kr", name: "Norwegian Krone" },
{ code: "NZD", local: "New Zealand", symbol: "NZ$", name: "New Zealand Dollar" },
{ code: "PLN", local: "Poland", symbol: "zł", name: "Polish Zloty" },
{ code: "RMB", local: "zh-CN", symbol: "¥", name: "Chinese Yuan" },
{ code: "RON", local: "ro-RO", symbol: "lei", name: "Romanian Leu" },
{ code: "RUB", local: "Russia", symbol: "₽", name: "Russian Ruble" },
{ code: "SAR", local: "Saudi Arabia", symbol: "﷼", name: "Saudi Riyal" },
{ code: "SEK", local: "Sweden", symbol: "kr", name: "Swedish Krona" },
{ code: "SGD", local: "Singapore", symbol: "S$", name: "Singapore Dollar" },
{ code: "THB", local: "Thailand", symbol: "฿", name: "Thai Baht" },
{ code: "TRY", local: "Turkey", symbol: "₺", name: "Turkish Lira" },
{ code: "USD", local: "United States", symbol: "$", name: "United States Dollar" },
{ code: "XAG", local: "Global", symbol: "XAG", name: "Silver Troy Ounce" },
{ code: "XAU", local: "Global", symbol: "XAU", name: "Gold Troy Ounce" },
{ code: "ZAR", local: "South Africa", symbol: "R", name: "South African Rand" },
];

View File

@ -1,8 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Detail } from "~~/components/global/DetailsSection/types"; import { Detail } from "~~/components/global/DetailsSection/types";
import { themes } from "~~/lib/data/themes"; import { themes } from "~~/lib/data/themes";
import { currencies, Currency } from "~~/lib/data/currency"; import { CurrenciesCurrency, NotifierCreate, NotifierOut } from "~~/lib/api/types/data-contracts";
import { NotifierCreate, NotifierOut } from "~~/lib/api/types/data-contracts";
definePageMeta({ definePageMeta({
middleware: ["auth"], middleware: ["auth"],
@ -15,9 +14,23 @@
const confirm = useConfirm(); const confirm = useConfirm();
const notify = useNotifier(); const notify = useNotifier();
// Currency Selection const currencies = computedAsync(async () => {
const currency = ref<Currency>(currencies[0]); const resp = await api.group.currencies();
if (resp.error) {
notify.error("Failed to get currencies");
return [];
}
return resp.data;
});
// Currency Selection
const currency = ref<CurrenciesCurrency>({
code: "USD",
name: "United States Dollar",
local: "en-US",
symbol: "$",
});
watch(currency, () => { watch(currency, () => {
if (group.value) { if (group.value) {
group.value.currency = currency.value.code; group.value.currency = currency.value.code;
@ -45,7 +58,7 @@
} }
// @ts-expect-error - typescript is stupid, it should know group.value is not null // @ts-expect-error - typescript is stupid, it should know group.value is not null
const found = currencies.find(c => c.code === group.value.currency); const found = currencies.value.find(c => c.code === group.value.currency);
if (found) { if (found) {
currency.value = found; currency.value = found;
} }