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.
"remoteUser": "node",
"features": {
"golang": "1.20"
"golang": "1.21"
}
}

View File

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

View File

@ -46,7 +46,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.20"
go-version: "1.21"
- uses: actions/setup-node@v4
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 {
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/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/internal/web/adapters"
"github.com/hay-kot/httpkit/errchain"
)
@ -52,6 +53,14 @@ func (ctrl *V1Controller) HandleGroupGet() errchain.HandlerFunc {
func (ctrl *V1Controller) HandleGroupUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, body repo.GroupUpdate) (repo.Group, error) {
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)
}

View File

@ -1,6 +1,7 @@
package main
import (
"bytes"
"context"
"fmt"
"net/http"
@ -13,6 +14,7 @@ import (
"github.com/go-chi/chi/v5"
"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/reporting/eventbus"
"github.com/hay-kot/homebox/backend/internal/data/ent"
@ -126,12 +128,40 @@ func run(cfg *config.Config) error {
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.db = c
app.repos = repo.New(c, app.bus, cfg.Storage.Data)
app.services = services.New(
app.repos,
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,
})))
r.Get(v1Base("/currencies"), chain.ToHandlerFunc(v1Ctrl.HandleCurrency()))
providers := []v1.AuthProvider{
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": {
"get": {
"security": [
@ -410,6 +429,16 @@ const docTemplate = `{
"description": "location Ids",
"name": "locations",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "parent Ids",
"name": "parentIds",
"in": "query"
}
],
"responses": {
@ -1574,7 +1603,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.ApiSummary"
"$ref": "#/definitions/v1.APISummary"
}
}
}
@ -1645,6 +1674,12 @@ const docTemplate = `{
"schema": {
"$ref": "#/definitions/v1.LoginForm"
}
},
{
"type": "string",
"description": "auth provider",
"name": "provider",
"in": "query"
}
],
"responses": {
@ -1823,6 +1858,23 @@ const docTemplate = `{
}
},
"definitions": {
"currencies.Currency": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"local": {
"type": "string"
},
"name": {
"type": "string"
},
"symbol": {
"type": "string"
}
}
},
"repo.DocumentOut": {
"type": "object",
"properties": {
@ -1998,12 +2050,6 @@ const docTemplate = `{
"$ref": "#/definitions/repo.ItemAttachment"
}
},
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemSummary"
}
},
"createdAt": {
"type": "string"
},
@ -2181,8 +2227,7 @@ const docTemplate = `{
"type": "boolean"
},
"assetId": {
"type": "string",
"example": "0"
"type": "string"
},
"description": {
"type": "string"
@ -2736,15 +2781,7 @@ const docTemplate = `{
}
}
},
"v1.ActionAmountResult": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.ApiSummary": {
"v1.APISummary": {
"type": "object",
"properties": {
"allowRegistration": {
@ -2773,6 +2810,14 @@ const docTemplate = `{
}
}
},
"v1.ActionAmountResult": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.Build": {
"type": "object",
"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": {
"get": {
"security": [
@ -403,6 +422,16 @@
"description": "location Ids",
"name": "locations",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "parent Ids",
"name": "parentIds",
"in": "query"
}
],
"responses": {
@ -1567,7 +1596,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.ApiSummary"
"$ref": "#/definitions/v1.APISummary"
}
}
}
@ -1638,6 +1667,12 @@
"schema": {
"$ref": "#/definitions/v1.LoginForm"
}
},
{
"type": "string",
"description": "auth provider",
"name": "provider",
"in": "query"
}
],
"responses": {
@ -1816,6 +1851,23 @@
}
},
"definitions": {
"currencies.Currency": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"local": {
"type": "string"
},
"name": {
"type": "string"
},
"symbol": {
"type": "string"
}
}
},
"repo.DocumentOut": {
"type": "object",
"properties": {
@ -1991,12 +2043,6 @@
"$ref": "#/definitions/repo.ItemAttachment"
}
},
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemSummary"
}
},
"createdAt": {
"type": "string"
},
@ -2174,8 +2220,7 @@
"type": "boolean"
},
"assetId": {
"type": "string",
"example": "0"
"type": "string"
},
"description": {
"type": "string"
@ -2729,15 +2774,7 @@
}
}
},
"v1.ActionAmountResult": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.ApiSummary": {
"v1.APISummary": {
"type": "object",
"properties": {
"allowRegistration": {
@ -2766,6 +2803,14 @@
}
}
},
"v1.ActionAmountResult": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.Build": {
"type": "object",
"properties": {

View File

@ -1,5 +1,16 @@
basePath: /api
definitions:
currencies.Currency:
properties:
code:
type: string
local:
type: string
name:
type: string
symbol:
type: string
type: object
repo.DocumentOut:
properties:
id:
@ -116,10 +127,6 @@ definitions:
items:
$ref: '#/definitions/repo.ItemAttachment'
type: array
children:
items:
$ref: '#/definitions/repo.ItemSummary'
type: array
createdAt:
type: string
description:
@ -238,7 +245,6 @@ definitions:
archived:
type: boolean
assetId:
example: "0"
type: string
description:
type: string
@ -608,12 +614,7 @@ definitions:
token:
type: string
type: object
v1.ActionAmountResult:
properties:
completed:
type: integer
type: object
v1.ApiSummary:
v1.APISummary:
properties:
allowRegistration:
type: boolean
@ -632,6 +633,11 @@ definitions:
type: string
type: array
type: object
v1.ActionAmountResult:
properties:
completed:
type: integer
type: object
v1.Build:
properties:
buildTime:
@ -789,6 +795,18 @@ paths:
summary: Get Item by Asset ID
tags:
- Items
/v1/currency:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/currencies.Currency'
summary: Currency
tags:
- Base
/v1/groups:
get:
produces:
@ -942,6 +960,13 @@ paths:
type: string
name: locations
type: array
- collectionFormat: multi
description: parent Ids
in: query
items:
type: string
name: parentIds
type: array
produces:
- application/json
responses:
@ -1656,7 +1681,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/v1.ApiSummary'
$ref: '#/definitions/v1.APISummary'
summary: Application Info
tags:
- Base
@ -1699,6 +1724,10 @@ paths:
required: true
schema:
$ref: '#/definitions/v1.LoginForm'
- description: auth provider
in: query
name: provider
type: string
produces:
- application/json
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
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/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
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/olahol/melody v1.1.4 h1:RQHfKZkQmDxI0+SLZRNBCn4LiXdqxLKRGSkT8Dyoe/E=
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/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
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/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
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
import (
"github.com/hay-kot/homebox/backend/internal/core/currencies"
"github.com/hay-kot/homebox/backend/internal/data/repo"
)
@ -10,12 +11,14 @@ type AllServices struct {
Group *GroupService
Items *ItemService
BackgroundService *BackgroundService
Currencies *currencies.CurrencyRegistry
}
type OptionsFunc func(*options)
type options struct {
autoIncrementAssetID bool
currencies []currencies.Currency
}
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 {
if repos == nil {
panic("repos cannot be nil")
}
defaultCurrencies, err := currencies.CollectionCurrencies(
currencies.CollectDefaults(),
)
if err != nil {
panic("failed to collect default currencies")
}
options := &options{
autoIncrementAssetID: true,
currencies: defaultCurrencies,
}
for _, opt := range opts {
@ -45,5 +62,6 @@ func New(repos *repo.AllRepos, opts ...OptionsFunc) *AllServices {
autoIncrementAssetID: options.autoIncrementAssetID,
},
BackgroundService: &BackgroundService{repos},
Currencies: currencies.NewCurrencyService(options.currencies),
}
}

View File

@ -6,6 +6,7 @@ import (
"os"
"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/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/repo"
@ -61,7 +62,12 @@ func TestMain(m *testing.M) {
tClient = client
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() }()
bootstrap()

View File

@ -25,7 +25,7 @@ type Group struct {
// Name holds the value of the "name" field.
Name string `json:"name,omitempty"`
// 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.
// The values are being populated by the GroupQuery when eager-loading is set.
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 {
return fmt.Errorf("unexpected type %T for field currency", values[i])
} else if value.Valid {
gr.Currency = group.Currency(value.String)
gr.Currency = value.String
}
default:
gr.selectValues.Set(columns[i], values[i])
@ -253,7 +253,7 @@ func (gr *Group) String() string {
builder.WriteString(gr.Name)
builder.WriteString(", ")
builder.WriteString("currency=")
builder.WriteString(fmt.Sprintf("%v", gr.Currency))
builder.WriteString(gr.Currency)
builder.WriteByte(')')
return builder.String()
}

View File

@ -3,7 +3,6 @@
package group
import (
"fmt"
"time"
"entgo.io/ent/dialect/sql"
@ -119,65 +118,12 @@ var (
UpdateDefaultUpdatedAt func() time.Time
// NameValidator is a validator for the "name" field. It is called by the builders before save.
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 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.
type OrderOption func(*sql.Selector)

View File

@ -71,6 +71,11 @@ func Name(v string) predicate.Group {
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.
func CreatedAtEQ(v time.Time) predicate.Group {
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.
func CurrencyEQ(v Currency) predicate.Group {
func CurrencyEQ(v string) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldCurrency, v))
}
// 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))
}
// 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...))
}
// 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...))
}
// 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.
func HasUsers() predicate.Group {
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.
func (gc *GroupCreate) SetCurrency(gr group.Currency) *GroupCreate {
gc.mutation.SetCurrency(gr)
func (gc *GroupCreate) SetCurrency(s string) *GroupCreate {
gc.mutation.SetCurrency(s)
return gc
}
// SetNillableCurrency sets the "currency" field if the given value is not nil.
func (gc *GroupCreate) SetNillableCurrency(gr *group.Currency) *GroupCreate {
if gr != nil {
gc.SetCurrency(*gr)
func (gc *GroupCreate) SetNillableCurrency(s *string) *GroupCreate {
if s != nil {
gc.SetCurrency(*s)
}
return gc
}
@ -267,11 +267,6 @@ func (gc *GroupCreate) check() error {
if _, ok := gc.mutation.Currency(); !ok {
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
}
@ -320,7 +315,7 @@ func (gc *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
_node.Name = value
}
if value, ok := gc.mutation.Currency(); ok {
_spec.SetField(group.FieldCurrency, field.TypeEnum, value)
_spec.SetField(group.FieldCurrency, field.TypeString, value)
_node.Currency = value
}
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.
func (gu *GroupUpdate) SetCurrency(gr group.Currency) *GroupUpdate {
gu.mutation.SetCurrency(gr)
func (gu *GroupUpdate) SetCurrency(s string) *GroupUpdate {
gu.mutation.SetCurrency(s)
return gu
}
// SetNillableCurrency sets the "currency" field if the given value is not nil.
func (gu *GroupUpdate) SetNillableCurrency(gr *group.Currency) *GroupUpdate {
if gr != nil {
gu.SetCurrency(*gr)
func (gu *GroupUpdate) SetNillableCurrency(s *string) *GroupUpdate {
if s != nil {
gu.SetCurrency(*s)
}
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)}
}
}
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
}
@ -397,7 +392,7 @@ func (gu *GroupUpdate) sqlSave(ctx context.Context) (n int, err error) {
_spec.SetField(group.FieldName, field.TypeString, value)
}
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() {
edge := &sqlgraph.EdgeSpec{
@ -755,15 +750,15 @@ func (guo *GroupUpdateOne) SetNillableName(s *string) *GroupUpdateOne {
}
// SetCurrency sets the "currency" field.
func (guo *GroupUpdateOne) SetCurrency(gr group.Currency) *GroupUpdateOne {
guo.mutation.SetCurrency(gr)
func (guo *GroupUpdateOne) SetCurrency(s string) *GroupUpdateOne {
guo.mutation.SetCurrency(s)
return guo
}
// SetNillableCurrency sets the "currency" field if the given value is not nil.
func (guo *GroupUpdateOne) SetNillableCurrency(gr *group.Currency) *GroupUpdateOne {
if gr != nil {
guo.SetCurrency(*gr)
func (guo *GroupUpdateOne) SetNillableCurrency(s *string) *GroupUpdateOne {
if s != nil {
guo.SetCurrency(*s)
}
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)}
}
}
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
}
@ -1125,7 +1115,7 @@ func (guo *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error
_spec.SetField(group.FieldName, field.TypeString, value)
}
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() {
edge := &sqlgraph.EdgeSpec{

View File

@ -117,7 +117,7 @@ var (
{Name: "created_at", Type: field.TypeTime},
{Name: "updated_at", Type: field.TypeTime},
{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 = &schema.Table{

View File

@ -2340,7 +2340,7 @@ type GroupMutation struct {
created_at *time.Time
updated_at *time.Time
name *string
currency *group.Currency
currency *string
clearedFields map[string]struct{}
users map[uuid.UUID]struct{}
removedusers map[uuid.UUID]struct{}
@ -2581,12 +2581,12 @@ func (m *GroupMutation) ResetName() {
}
// SetCurrency sets the "currency" field.
func (m *GroupMutation) SetCurrency(gr group.Currency) {
m.currency = &gr
func (m *GroupMutation) SetCurrency(s string) {
m.currency = &s
}
// 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
if v == nil {
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.
// 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.
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) {
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)
return nil
case group.FieldCurrency:
v, ok := value.(group.Currency)
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}

View File

@ -161,6 +161,10 @@ func init() {
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 := groupMixinFields0[0].Descriptor()
// 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").
MaxLen(255).
NotEmpty(),
field.Enum("currency").
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",
),
field.String("currency").
Default("usd"),
}
}

View File

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

View File

@ -28,7 +28,7 @@ func NewGroupRepository(db *ent.Client) *GroupRepository {
Name: g.Name,
CreatedAt: g.CreatedAt,
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) {
currency := group.Currency(strings.ToLower(data.Currency))
entity, err := r.db.Group.UpdateOneID(ID).
SetName(data.Name).
SetCurrency(currency).
SetCurrency(strings.ToLower(data.Currency)).
Save(ctx)
return r.groupMapper.MapErr(entity, err)

View File

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

View File

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

View File

@ -88,7 +88,7 @@ func (fe FieldErrors) Nil() bool {
return len(fe) == 0
}
// Error implments the error interface.
// Error implements the error interface.
func (fe FieldErrors) Error() string {
d, err := json.Marshal(fe)
if err != nil {
@ -101,6 +101,10 @@ func NewFieldErrors(errs ...FieldError) FieldErrors {
return errs
}
func NewFieldError(field, reason string) FieldError {
return FieldError{Field: field, Error: reason}
}
func IsFieldError(err error) bool {
v := FieldErrors{}
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": {
"get": {
"security": [
@ -403,6 +422,16 @@
"description": "location Ids",
"name": "locations",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "parent Ids",
"name": "parentIds",
"in": "query"
}
],
"responses": {
@ -1567,7 +1596,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.ApiSummary"
"$ref": "#/definitions/v1.APISummary"
}
}
}
@ -1638,6 +1667,12 @@
"schema": {
"$ref": "#/definitions/v1.LoginForm"
}
},
{
"type": "string",
"description": "auth provider",
"name": "provider",
"in": "query"
}
],
"responses": {
@ -1816,6 +1851,23 @@
}
},
"definitions": {
"currencies.Currency": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"local": {
"type": "string"
},
"name": {
"type": "string"
},
"symbol": {
"type": "string"
}
}
},
"repo.DocumentOut": {
"type": "object",
"properties": {
@ -1991,12 +2043,6 @@
"$ref": "#/definitions/repo.ItemAttachment"
}
},
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemSummary"
}
},
"createdAt": {
"type": "string"
},
@ -2174,8 +2220,7 @@
"type": "boolean"
},
"assetId": {
"type": "string",
"example": "0"
"type": "string"
},
"description": {
"type": "string"
@ -2729,15 +2774,7 @@
}
}
},
"v1.ActionAmountResult": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.ApiSummary": {
"v1.APISummary": {
"type": "object",
"properties": {
"allowRegistration": {
@ -2766,6 +2803,14 @@
}
}
},
"v1.ActionAmountResult": {
"type": "object",
"properties": {
"completed": {
"type": "integer"
}
}
},
"v1.Build": {
"type": "object",
"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_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_CURRENCY_CONFIG | | json configuration file containing additional currencie |
| 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_WRITE_TIMEOUT | 10 | Write timeout of HTTP server |
@ -104,6 +105,7 @@ volumes:
--debug-port/$HBOX_DEBUG_PORT <string> (default: 4000)
--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-currency-config/$HBOX_OPTIONS_CURRENCY_CONFIG <string>
--help/-h
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.**
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 { factories } from "../factories";
import { sharedUserClient } from "../test-utils";
import { currencies } from "~~/lib/data/currency";
describe("first time user workflow (register, login, join group)", () => {
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");
});
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 () => {
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", () => {
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.
*/
async function useLocation(api: UserClient): Promise<[LocationOut, () => Promise<void>]> {

View File

@ -1,5 +1,11 @@
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 {
createInvitation(data: GroupInvitationCreate) {
@ -21,4 +27,10 @@ export class GroupApi extends BaseAPI {
url: route("/groups"),
});
}
currencies() {
return this.http.get<CurrenciesCurrency[]>({
url: route("/currencies"),
});
}
}

View File

@ -1,5 +1,5 @@
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 = {
health: boolean;
@ -10,7 +10,7 @@ export type StatusResult = {
export class PublicApi extends BaseAPI {
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) {

View File

@ -10,6 +10,13 @@
* ---------------------------------------------------------------
*/
export interface CurrenciesCurrency {
code: string;
local: string;
name: string;
symbol: string;
}
export interface DocumentOut {
id: string;
path: string;
@ -81,7 +88,6 @@ export interface ItemOut {
/** @example "0" */
assetId: string;
attachments: ItemAttachment[];
children: ItemSummary[];
createdAt: Date | string;
description: string;
fields: ItemField[];
@ -141,7 +147,6 @@ export interface ItemSummary {
export interface ItemUpdate {
archived: boolean;
/** @example "0" */
assetId: string;
description: string;
fields: ItemField[];
@ -364,11 +369,7 @@ export interface UserRegistration {
token: string;
}
export interface ActionAmountResult {
completed: number;
}
export interface ApiSummary {
export interface APISummary {
allowRegistration: boolean;
build: Build;
demo: boolean;
@ -378,6 +379,10 @@ export interface ApiSummary {
versions: string[];
}
export interface ActionAmountResult {
completed: number;
}
export interface Build {
buildTime: 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">
import { Detail } from "~~/components/global/DetailsSection/types";
import { themes } from "~~/lib/data/themes";
import { currencies, Currency } from "~~/lib/data/currency";
import { NotifierCreate, NotifierOut } from "~~/lib/api/types/data-contracts";
import { CurrenciesCurrency, NotifierCreate, NotifierOut } from "~~/lib/api/types/data-contracts";
definePageMeta({
middleware: ["auth"],
@ -15,9 +14,23 @@
const confirm = useConfirm();
const notify = useNotifier();
// Currency Selection
const currency = ref<Currency>(currencies[0]);
const currencies = computedAsync(async () => {
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, () => {
if (group.value) {
group.value.currency = currency.value.code;
@ -45,7 +58,7 @@
}
// @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) {
currency.value = found;
}