support user custom exchange rates data

This commit is contained in:
MaysWind
2025-05-26 00:47:19 +08:00
parent c4d20c539f
commit 817291c9a7
41 changed files with 1257 additions and 73 deletions
+25 -16
View File
@@ -20,14 +20,15 @@ const pageCountForDataExport = 1000
// DataManagementsApi represents data management api
type DataManagementsApi struct {
ApiUsingConfig
tokens *services.TokenService
users *services.UserService
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
tags *services.TransactionTagService
pictures *services.TransactionPictureService
templates *services.TransactionTemplateService
tokens *services.TokenService
users *services.UserService
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
tags *services.TransactionTagService
pictures *services.TransactionPictureService
templates *services.TransactionTemplateService
userCustomExchangeRates *services.UserCustomExchangeRatesService
}
// Initialize a data management api singleton instance
@@ -36,14 +37,15 @@ var (
ApiUsingConfig: ApiUsingConfig{
container: settings.Container,
},
tokens: services.Tokens,
users: services.Users,
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
tags: services.TransactionTags,
pictures: services.TransactionPictures,
templates: services.TransactionTemplates,
tokens: services.Tokens,
users: services.Users,
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
tags: services.TransactionTags,
pictures: services.TransactionPictures,
templates: services.TransactionTemplates,
userCustomExchangeRates: services.UserCustomExchangeRates,
}
)
@@ -179,6 +181,13 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.WebContext) (any, *errs.Er
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.userCustomExchangeRates.DeleteAllCustomExchangeRates(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all user custom exchange rates, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[data_managements.ClearDataHandler] user \"uid:%d\" has cleared all data", uid)
return true, nil
}
+73
View File
@@ -4,12 +4,17 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/exchangerates"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// ExchangeRatesApi represents exchange rate api
type ExchangeRatesApi struct {
ApiUsingConfig
users *services.UserService
userCustomExchangeRates *services.UserCustomExchangeRatesService
}
// Initialize a exchange rate api singleton instance
@@ -18,6 +23,8 @@ var (
ApiUsingConfig: ApiUsingConfig{
container: settings.Container,
},
users: services.Users,
userCustomExchangeRates: services.UserCustomExchangeRates,
}
)
@@ -37,3 +44,69 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.WebContext) (any, *
return exchangeRateResponse, nil
}
// UserCustomExchangeRateUpdateHandler updates user custom exchange rates data by request parameters for current user
func (a *ExchangeRatesApi) UserCustomExchangeRateUpdateHandler(c *core.WebContext) (any, *errs.Error) {
var customExchangeRateUpdateReq models.UserCustomExchangeRateUpdateRequest
err := c.ShouldBindJSON(&customExchangeRateUpdateReq)
if err != nil {
log.Warnf(c, "[exchange_rates.UserCustomExchangeRateUpdateHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.Errorf(c, "[exchange_rates.UserCustomExchangeRateUpdateHandler] failed to get user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if customExchangeRateUpdateReq.Currency == user.DefaultCurrency {
return nil, errs.ErrCannotUpdateExchangeRateForDefaultCurrency
}
newCustomExchangeRate, defaultCurrencyExchangeRate, err := a.userCustomExchangeRates.UpdateCustomExchangeRate(c, uid, customExchangeRateUpdateReq.Currency, customExchangeRateUpdateReq.Rate, user.DefaultCurrency)
if err != nil {
log.Errorf(c, "[exchange_rates.UserCustomExchangeRateUpdateHandler] failed to update user custom exchange rate \"currency:%s\" for user \"uid:%d\", because %s", customExchangeRateUpdateReq.Currency, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[exchange_rates.UserCustomExchangeRateUpdateHandler] user \"uid:%d\" has updated user custom exchange rate \"currency:%s\" successfully", uid, customExchangeRateUpdateReq.Currency)
return newCustomExchangeRate.ToUserCustomExchangeRateUpdateResponse(defaultCurrencyExchangeRate.Rate), nil
}
// UserCustomExchangeRateDeleteHandler deletes an existed user custom exchange rates data by request parameters for current user
func (a *ExchangeRatesApi) UserCustomExchangeRateDeleteHandler(c *core.WebContext) (any, *errs.Error) {
var customExchangeRateDeleteReq models.UserCustomExchangeRateDeleteRequest
err := c.ShouldBindJSON(&customExchangeRateDeleteReq)
if err != nil {
log.Warnf(c, "[exchange_rates.UserCustomExchangeRateDeleteHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.Errorf(c, "[exchange_rates.UserCustomExchangeRateDeleteHandler] failed to get user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if customExchangeRateDeleteReq.Currency == user.DefaultCurrency {
return nil, errs.ErrCannotDeleteExchangeRateForDefaultCurrency
}
err = a.userCustomExchangeRates.DeleteCustomExchangeRate(c, uid, customExchangeRateDeleteReq.Currency)
if err != nil {
log.Errorf(c, "[exchange_rates.UserCustomExchangeRateDeleteHandler] failed to delete user custom exchange rate \"currency:%s\" for user \"uid:%d\", because %s", customExchangeRateDeleteReq.Currency, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[exchange_rates.UserCustomExchangeRateDeleteHandler] user \"uid:%d\" has deleted user custom exchange rate \"currency:%s\"", uid, customExchangeRateDeleteReq.Currency)
return true, nil
}
+14 -13
View File
@@ -25,19 +25,20 @@ const (
// Sub categories of normal error
const (
NormalSubcategoryGlobal = 0
NormalSubcategoryUser = 1
NormalSubcategoryToken = 2
NormalSubcategoryTwofactor = 3
NormalSubcategoryAccount = 4
NormalSubcategoryTransaction = 5
NormalSubcategoryCategory = 6
NormalSubcategoryTag = 7
NormalSubcategoryDataManagement = 8
NormalSubcategoryMapProxy = 9
NormalSubcategoryTemplate = 10
NormalSubcategoryPicture = 11
NormalSubcategoryConverter = 12
NormalSubcategoryGlobal = 0
NormalSubcategoryUser = 1
NormalSubcategoryToken = 2
NormalSubcategoryTwofactor = 3
NormalSubcategoryAccount = 4
NormalSubcategoryTransaction = 5
NormalSubcategoryCategory = 6
NormalSubcategoryTag = 7
NormalSubcategoryDataManagement = 8
NormalSubcategoryMapProxy = 9
NormalSubcategoryTemplate = 10
NormalSubcategoryPicture = 11
NormalSubcategoryConverter = 12
NormalSubcategoryUserCustomExchangeRate = 13
)
// Error represents the specific error returned to user
+10
View File
@@ -0,0 +1,10 @@
package errs
import "net/http"
// Error codes related to user custom exchange rates
var (
ErrUserCustomExchangeRateNotFound = NewNormalError(NormalSubcategoryUserCustomExchangeRate, 0, http.StatusBadRequest, "user custom exchange rate data not found")
ErrCannotUpdateExchangeRateForDefaultCurrency = NewNormalError(NormalSubcategoryUserCustomExchangeRate, 1, http.StatusBadRequest, "cannot update exchange rate data for base currency")
ErrCannotDeleteExchangeRateForDefaultCurrency = NewNormalError(NormalSubcategoryUserCustomExchangeRate, 2, http.StatusBadRequest, "cannot delete exchange rate data for base currency")
)
@@ -68,6 +68,9 @@ func InitializeExchangeRatesDataSource(config *settings.Config) error {
} else if config.ExchangeRatesDataSource == settings.InternationalMonetaryFundDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&InternationalMonetaryFundDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.UserCustomExchangeRatesDataSource {
Container.Current = newUserCustomExchangeRatesDataSource()
return nil
}
return errs.ErrInvalidExchangeRatesDataSource
+101
View File
@@ -0,0 +1,101 @@
package exchangerates
import (
"sort"
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
const userDataSourceType = "user_custom"
// UserCustomExchangeRatesDataSource defines the structure of user custom exchange rates data source
type UserCustomExchangeRatesDataSource struct {
ExchangeRatesDataSource
users *services.UserService
userCustomExchangeRates *services.UserCustomExchangeRatesService
}
func (e *UserCustomExchangeRatesDataSource) GetLatestExchangeRates(c core.Context, uid int64, currentConfig *settings.Config) (*models.LatestExchangeRateResponse, error) {
user, err := e.users.GetUserById(c, uid)
if err != nil {
log.Errorf(c, "[user_custom_datasource.GetLatestExchangeRates] failed to get user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
customExchangeRates, err := e.userCustomExchangeRates.GetAllCustomExchangeRatesByUid(c, uid)
if err != nil {
log.Errorf(c, "[user_custom_datasource.GetLatestExchangeRates] failed to get user custom exchange rates for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
baseCurrencyRate := int64(0)
hasDefaultCurrencyRate := false
for i := 0; i < len(customExchangeRates); i++ {
customExchangeRate := customExchangeRates[i]
if customExchangeRate.Currency == user.DefaultCurrency {
baseCurrencyRate = customExchangeRate.Rate
hasDefaultCurrencyRate = true
break
}
}
allExchangeRates := make(models.LatestExchangeRateSlice, 0, len(customExchangeRates))
latestUpdateTime := int64(0)
for i := 0; i < len(customExchangeRates); i++ {
customExchangeRate := customExchangeRates[i]
if _, exists := validators.AllCurrencyNames[customExchangeRate.Currency]; !exists {
continue
}
if customExchangeRate.UpdatedUnixTime > latestUpdateTime {
latestUpdateTime = customExchangeRate.UpdatedUnixTime
}
if hasDefaultCurrencyRate && baseCurrencyRate > 0 {
allExchangeRates = append(allExchangeRates, customExchangeRate.ToLatestExchangeRate(baseCurrencyRate))
}
}
sort.Sort(allExchangeRates)
if latestUpdateTime < 1 {
latestUpdateTime = time.Now().Unix()
}
if !hasDefaultCurrencyRate {
allExchangeRates = append(allExchangeRates, &models.LatestExchangeRate{
Currency: user.DefaultCurrency,
Rate: "1",
})
}
finalExchangeRateResponse := &models.LatestExchangeRateResponse{
DataSource: userDataSourceType,
ReferenceUrl: "",
UpdateTime: latestUpdateTime,
BaseCurrency: user.DefaultCurrency,
ExchangeRates: allExchangeRates,
}
return finalExchangeRateResponse, nil
}
func newUserCustomExchangeRatesDataSource() *UserCustomExchangeRatesDataSource {
return &UserCustomExchangeRatesDataSource{
users: services.Users,
userCustomExchangeRates: services.UserCustomExchangeRates,
}
}
+81 -1
View File
@@ -1,6 +1,39 @@
package models
import "strings"
import (
"strings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const UserCustomExchangeRateFactorInDatabase = int64(100000000)
// UserCustomExchangeRate represents user custom exchange rate data
type UserCustomExchangeRate struct {
Uid int64 `xorm:"PK NOT NULL"`
DeletedUnixTime int64 `xorm:"PK NOT NULL"`
Currency string `xorm:"PK VARCHAR(3) NOT NULL"`
Rate int64 `xorm:"NOT NULL"`
CreatedUnixTime int64
UpdatedUnixTime int64
}
// UserCustomExchangeRateUpdateRequest represents all parameters of user custom exchange rate data updating request
type UserCustomExchangeRateUpdateRequest struct {
Currency string `json:"currency" binding:"required,len=3,validCurrency"`
Rate string `json:"rate"`
}
// UserCustomExchangeRateDeleteRequest represents all parameters of user custom exchange rate data deleting request
type UserCustomExchangeRateDeleteRequest struct {
Currency string `json:"currency" binding:"required,len=3,validCurrency"`
}
// UserCustomExchangeRateUpdateResponse represents a view-object of the result of updating user custom exchange rate data
type UserCustomExchangeRateUpdateResponse struct {
LatestExchangeRate
UpdateTime int64 `json:"updateTime"`
}
// LatestExchangeRateResponse returns a view-object which contains latest exchange rate
type LatestExchangeRateResponse struct {
@@ -17,6 +50,53 @@ type LatestExchangeRate struct {
Rate string `json:"rate"`
}
// ToLatestExchangeRate returns a data pair of currency and exchange rate according to database model
func (r *UserCustomExchangeRate) ToLatestExchangeRate(baseCurrencyRate int64) *LatestExchangeRate {
rate := float64(0)
if baseCurrencyRate > 0 {
rate = float64(r.Rate) / float64(baseCurrencyRate)
}
return &LatestExchangeRate{
Currency: r.Currency,
Rate: utils.Float64ToString(rate),
}
}
// ToUserCustomExchangeRateUpdateResponse returns a view-object of the result of updating user custom exchange rate data according to database model
func (r *UserCustomExchangeRate) ToUserCustomExchangeRateUpdateResponse(baseCurrencyRate int64) *UserCustomExchangeRateUpdateResponse {
return &UserCustomExchangeRateUpdateResponse{
LatestExchangeRate: *r.ToLatestExchangeRate(baseCurrencyRate),
UpdateTime: r.UpdatedUnixTime,
}
}
// CreateUserCustomExchangeRate returns a user custom exchange rate database model according to currency and rate
func CreateUserCustomExchangeRate(uid int64, currency string, exchangeRate string, baseCurrencyRate int64) (*UserCustomExchangeRate, error) {
if baseCurrencyRate <= 0 {
return &UserCustomExchangeRate{
Uid: uid,
Currency: currency,
Rate: UserCustomExchangeRateFactorInDatabase,
}, nil
}
rate, err := utils.StringToFloat64(exchangeRate)
if err != nil {
return nil, err
}
rate = rate * float64(baseCurrencyRate)
return &UserCustomExchangeRate{
Uid: uid,
Currency: currency,
Rate: int64(rate),
}, nil
}
// LatestExchangeRateSlice represents the slice data structure of LatestExchangeRate
type LatestExchangeRateSlice []*LatestExchangeRate
+153
View File
@@ -0,0 +1,153 @@
package services
import (
"time"
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// UserCustomExchangeRatesService represents user custom exchange rate data service
type UserCustomExchangeRatesService struct {
ServiceUsingDB
}
// Initialize a user custom exchange rate data service singleton instance
var (
UserCustomExchangeRates = &UserCustomExchangeRatesService{
ServiceUsingDB: ServiceUsingDB{
container: datastore.Container,
},
}
)
// GetAllCustomExchangeRatesByUid returns all user exchange rate data models of user
func (s *UserCustomExchangeRatesService) GetAllCustomExchangeRatesByUid(c core.Context, uid int64) ([]*models.UserCustomExchangeRate, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
var customExchangeRates []*models.UserCustomExchangeRate
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted_unix_time=?", uid, 0).Find(&customExchangeRates)
return customExchangeRates, err
}
// UpdateCustomExchangeRate updates user exchange rate data model to database
func (s *UserCustomExchangeRatesService) UpdateCustomExchangeRate(c core.Context, uid int64, currency string, rate string, defaultCurrency string) (*models.UserCustomExchangeRate, *models.UserCustomExchangeRate, error) {
if uid <= 0 {
return nil, nil, errs.ErrUserIdInvalid
}
now := time.Now().Unix()
newCustomExchangeRate := &models.UserCustomExchangeRate{}
defaultCurrencyExchangeRate := &models.UserCustomExchangeRate{}
err := s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
oldCustomExchangeRate := &models.UserCustomExchangeRate{}
has, err := sess.Where("uid=? AND deleted_unix_time=? AND currency=?", uid, 0, currency).Get(oldCustomExchangeRate)
if err != nil {
return err
}
if has {
updateOldExchangeRateModel := &models.UserCustomExchangeRate{
DeletedUnixTime: now,
}
_, err = sess.Cols("deleted_unix_time").Where("uid=? AND deleted_unix_time=? AND currency=?", uid, 0, currency).Update(updateOldExchangeRateModel)
if err != nil {
return err
}
}
if currency != defaultCurrency {
has, err := sess.Where("uid=? AND deleted_unix_time=? AND currency=?", uid, 0, defaultCurrency).Get(defaultCurrencyExchangeRate)
if err != nil {
return err
}
if !has {
defaultCurrencyExchangeRate, _ = models.CreateUserCustomExchangeRate(uid, defaultCurrency, "1", 0)
defaultCurrencyExchangeRate.CreatedUnixTime = now
defaultCurrencyExchangeRate.UpdatedUnixTime = now
defaultCurrencyExchangeRate.DeletedUnixTime = 0
_, err = sess.Insert(defaultCurrencyExchangeRate)
if err != nil {
return err
}
}
} else {
defaultCurrencyExchangeRate = oldCustomExchangeRate
}
newCustomExchangeRate, err = models.CreateUserCustomExchangeRate(uid, currency, rate, defaultCurrencyExchangeRate.Rate)
newCustomExchangeRate.CreatedUnixTime = now
newCustomExchangeRate.UpdatedUnixTime = now
newCustomExchangeRate.DeletedUnixTime = 0
_, err = sess.Insert(newCustomExchangeRate)
return err
})
if err != nil {
return nil, nil, err
}
return newCustomExchangeRate, defaultCurrencyExchangeRate, err
}
// DeleteCustomExchangeRate deletes an existed user exchange rate data from database
func (s *UserCustomExchangeRatesService) DeleteCustomExchangeRate(c core.Context, uid int64, currency string) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
now := time.Now().Unix()
updateModel := &models.UserCustomExchangeRate{
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
deletedRows, err := sess.Cols("deleted_unix_time").Where("uid=? AND deleted_unix_time=? AND currency=?", uid, 0, currency).Update(updateModel)
if err != nil {
return err
} else if deletedRows < 1 {
return errs.ErrUserCustomExchangeRateNotFound
}
return err
})
}
// DeleteAllCustomExchangeRates deletes all existed user exchange rate data from database
func (s *UserCustomExchangeRatesService) DeleteAllCustomExchangeRates(c core.Context, uid int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
now := time.Now().Unix()
updateModel := &models.UserCustomExchangeRate{
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Cols("deleted_unix_time").Where("uid=? AND deleted_unix_time=?", uid, 0).Update(updateModel)
if err != nil {
return err
}
return nil
})
}
+3 -1
View File
@@ -117,6 +117,7 @@ const (
NationalBankOfUkraineDataSource string = "national_bank_of_ukraine"
CentralBankOfUzbekistanDataSource string = "central_bank_of_uzbekistan"
InternationalMonetaryFundDataSource string = "international_monetary_fund"
UserCustomExchangeRatesDataSource string = "user_custom"
)
const (
@@ -914,7 +915,8 @@ func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectio
dataSource == SwissNationalBankDataSource ||
dataSource == NationalBankOfUkraineDataSource ||
dataSource == CentralBankOfUzbekistanDataSource ||
dataSource == InternationalMonetaryFundDataSource {
dataSource == InternationalMonetaryFundDataSource ||
dataSource == UserCustomExchangeRatesDataSource {
config.ExchangeRatesDataSource = dataSource
} else {
return errs.ErrInvalidExchangeRatesDataSource