support scheduled transaction (#2)

This commit is contained in:
MaysWind
2024-08-26 01:52:52 +08:00
parent 17d4fec256
commit d2eaf5c6da
42 changed files with 1437 additions and 112 deletions
+13 -5
View File
@@ -97,12 +97,20 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.WebContext) (any, *er
return nil, errs.ErrOperationFailed
}
totalScheduledTransactionCount, err := a.templates.GetTotalScheduledTemplateCountByUid(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.DataStatisticsHandler] failed to get total scheduled transaction count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
dataStatisticsResp := &models.DataStatisticsResponse{
TotalAccountCount: totalAccountCount,
TotalTransactionCategoryCount: totalTransactionCategoryCount,
TotalTransactionTagCount: totalTransactionTagCount,
TotalTransactionCount: totalTransactionCount,
TotalTransactionTemplateCount: totalTransactionTemplateCount,
TotalAccountCount: totalAccountCount,
TotalTransactionCategoryCount: totalTransactionCategoryCount,
TotalTransactionTagCount: totalTransactionTagCount,
TotalTransactionCount: totalTransactionCount,
TotalTransactionTemplateCount: totalTransactionTemplateCount,
TotalScheduledTransactionCount: totalScheduledTransactionCount,
}
return dataStatisticsResp, nil
+159 -4
View File
@@ -3,6 +3,7 @@ package api
import (
"sort"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
@@ -44,11 +45,15 @@ func (a *TransactionTemplatesApi) TemplateListHandler(c *core.WebContext) (any,
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
if templateListReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateListReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_NORMAL {
if templateListReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateListReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
log.Warnf(c, "[transaction_templates.TemplateListHandler] template type invalid, type is %d", templateListReq.TemplateType)
return nil, errs.ErrTransactionTemplateTypeInvalid
}
if templateListReq.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
return nil, errs.ErrScheduledTransactionNotEnabled
}
uid := c.GetCurrentUid()
templates, err := a.templates.GetAllTemplatesByUid(c, uid, templateListReq.TemplateType)
@@ -87,6 +92,10 @@ func (a *TransactionTemplatesApi) TemplateGetHandler(c *core.WebContext) (any, *
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
return nil, errs.ErrScheduledTransactionNotEnabled
}
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
@@ -103,16 +112,34 @@ func (a *TransactionTemplatesApi) TemplateCreateHandler(c *core.WebContext) (any
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
if templateCreateReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateCreateReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_NORMAL {
if templateCreateReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateCreateReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
log.Warnf(c, "[transaction_templates.TemplateCreateHandler] template type invalid, type is %d", templateCreateReq.TemplateType)
return nil, errs.ErrTransactionTemplateTypeInvalid
}
if templateCreateReq.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
return nil, errs.ErrScheduledTransactionNotEnabled
}
if templateCreateReq.Type <= models.TRANSACTION_TYPE_MODIFY_BALANCE || templateCreateReq.Type > models.TRANSACTION_TYPE_TRANSFER {
log.Warnf(c, "[transaction_templates.TemplateCreateHandler] transaction type invalid, type is %d", templateCreateReq.Type)
return nil, errs.ErrTransactionTypeInvalid
}
if templateCreateReq.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
if templateCreateReq.ScheduledFrequencyType == nil ||
templateCreateReq.ScheduledFrequency == nil ||
templateCreateReq.ScheduledTimezoneUtcOffset == nil {
return nil, errs.ErrScheduledTransactionFrequencyInvalid
}
if *templateCreateReq.ScheduledFrequencyType == models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED && *templateCreateReq.ScheduledFrequency != "" {
return nil, errs.ErrScheduledTransactionFrequencyInvalid
} else if *templateCreateReq.ScheduledFrequencyType != models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED && *templateCreateReq.ScheduledFrequency == "" {
return nil, errs.ErrScheduledTransactionFrequencyInvalid
}
}
uid := c.GetCurrentUid()
maxOrderId, err := a.templates.GetMaxDisplayOrder(c, uid, templateCreateReq.TemplateType)
@@ -185,6 +212,24 @@ func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.WebContext) (any
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
return nil, errs.ErrScheduledTransactionNotEnabled
}
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
if templateModifyReq.ScheduledFrequencyType == nil ||
templateModifyReq.ScheduledFrequency == nil ||
templateModifyReq.ScheduledTimezoneUtcOffset == nil {
return nil, errs.ErrScheduledTransactionFrequencyInvalid
}
if *templateModifyReq.ScheduledFrequencyType == models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED && *templateModifyReq.ScheduledFrequency != "" {
return nil, errs.ErrScheduledTransactionFrequencyInvalid
} else if *templateModifyReq.ScheduledFrequencyType != models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED && *templateModifyReq.ScheduledFrequency == "" {
return nil, errs.ErrScheduledTransactionFrequencyInvalid
}
}
newTemplate := &models.TransactionTemplate{
TemplateId: template.TemplateId,
Uid: uid,
@@ -200,6 +245,13 @@ func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.WebContext) (any
Comment: templateModifyReq.Comment,
}
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
newTemplate.ScheduledFrequencyType = *templateModifyReq.ScheduledFrequencyType
newTemplate.ScheduledFrequency = a.getOrderedFrequencyValues(*templateModifyReq.ScheduledFrequency)
newTemplate.ScheduledAt = a.getUTCScheduledAt(*templateModifyReq.ScheduledTimezoneUtcOffset)
newTemplate.ScheduledTimezoneUtcOffset = *templateModifyReq.ScheduledTimezoneUtcOffset
}
if newTemplate.Name == template.Name &&
newTemplate.Type == template.Type &&
newTemplate.CategoryId == template.CategoryId &&
@@ -210,7 +262,16 @@ func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.WebContext) (any
newTemplate.RelatedAccountAmount == template.RelatedAccountAmount &&
newTemplate.HideAmount == template.HideAmount &&
newTemplate.Comment == template.Comment {
return nil, errs.ErrNothingWillBeUpdated
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_NORMAL {
return nil, errs.ErrNothingWillBeUpdated
} else if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
if newTemplate.ScheduledFrequencyType == template.ScheduledFrequencyType &&
newTemplate.ScheduledFrequency == template.ScheduledFrequency &&
newTemplate.ScheduledAt == template.ScheduledAt &&
newTemplate.ScheduledTimezoneUtcOffset == template.ScheduledTimezoneUtcOffset {
return nil, errs.ErrNothingWillBeUpdated
}
}
}
err = a.templates.ModifyTemplate(c, newTemplate)
@@ -242,6 +303,18 @@ func (a *TransactionTemplatesApi) TemplateHideHandler(c *core.WebContext) (any,
}
uid := c.GetCurrentUid()
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateHideReq.Id)
if err != nil {
log.Errorf(c, "[transaction_templates.TemplateHideHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateHideReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
return nil, errs.ErrScheduledTransactionNotEnabled
}
err = a.templates.HideTemplate(c, uid, []int64{templateHideReq.Id}, templateHideReq.Hidden)
if err != nil {
@@ -264,6 +337,20 @@ func (a *TransactionTemplatesApi) TemplateMoveHandler(c *core.WebContext) (any,
}
uid := c.GetCurrentUid()
if len(templateMoveReq.NewDisplayOrders) > 0 {
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateMoveReq.NewDisplayOrders[0].Id)
if err != nil {
log.Errorf(c, "[transaction_templates.TemplateMoveHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateMoveReq.NewDisplayOrders[0].Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
return nil, errs.ErrScheduledTransactionNotEnabled
}
}
templates := make([]*models.TransactionTemplate, len(templateMoveReq.NewDisplayOrders))
for i := 0; i < len(templateMoveReq.NewDisplayOrders); i++ {
@@ -299,6 +386,18 @@ func (a *TransactionTemplatesApi) TemplateDeleteHandler(c *core.WebContext) (any
}
uid := c.GetCurrentUid()
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateDeleteReq.Id)
if err != nil {
log.Errorf(c, "[transaction_templates.TemplateDeleteHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateDeleteReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE && !a.CurrentConfig().EnableScheduledTransaction {
return nil, errs.ErrScheduledTransactionNotEnabled
}
err = a.templates.DeleteTemplate(c, uid, templateDeleteReq.Id)
if err != nil {
@@ -311,7 +410,7 @@ func (a *TransactionTemplatesApi) TemplateDeleteHandler(c *core.WebContext) (any
}
func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCreateReq *models.TransactionTemplateCreateRequest, order int32) *models.TransactionTemplate {
return &models.TransactionTemplate{
template := &models.TransactionTemplate{
Uid: uid,
TemplateType: templateCreateReq.TemplateType,
Name: templateCreateReq.Name,
@@ -326,4 +425,60 @@ func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCrea
Comment: templateCreateReq.Comment,
DisplayOrder: order,
}
if templateCreateReq.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
template.ScheduledFrequencyType = *templateCreateReq.ScheduledFrequencyType
template.ScheduledFrequency = a.getOrderedFrequencyValues(*templateCreateReq.ScheduledFrequency)
template.ScheduledAt = a.getUTCScheduledAt(*templateCreateReq.ScheduledTimezoneUtcOffset)
template.ScheduledTimezoneUtcOffset = *templateCreateReq.ScheduledTimezoneUtcOffset
}
return template
}
func (a *TransactionTemplatesApi) getUTCScheduledAt(scheduledTimezoneUtcOffset int16) int16 {
templateTimeZone := time.FixedZone("Template Timezone", int(scheduledTimezoneUtcOffset)*60)
transactionTime := time.Date(2020, 1, 1, 0, 0, 0, 0, templateTimeZone)
transactionTimeInUTC := transactionTime.In(time.UTC)
minutesElapsedOfDayInUtc := transactionTimeInUTC.Hour()*60 + transactionTimeInUTC.Minute()
return int16(minutesElapsedOfDayInUtc)
}
func (a *TransactionTemplatesApi) getOrderedFrequencyValues(frequencyValue string) string {
if frequencyValue == "" {
return ""
}
items := strings.Split(frequencyValue, ",")
values := make([]int, 0, len(items))
valueExistMap := make(map[int]bool)
for i := 0; i < len(items); i++ {
value, err := utils.StringToInt(items[i])
if err != nil {
continue
}
if _, exists := valueExistMap[value]; !exists {
values = append(values, value)
valueExistMap[value] = true
}
}
sort.Ints(values)
var sortedFrequencyValueBuilder strings.Builder
for i := 0; i < len(values); i++ {
if sortedFrequencyValueBuilder.Len() > 0 {
sortedFrequencyValueBuilder.WriteRune(',')
}
sortedFrequencyValueBuilder.WriteString(utils.IntToString(values[i]))
}
return sortedFrequencyValueBuilder.String()
}
+11 -4
View File
@@ -10,7 +10,8 @@ import (
// CronContext represents the cron job context
type CronContext struct {
context.Context
contextId string
contextId string
cronJobInterval time.Duration
}
// GetContextId returns the current context id
@@ -18,11 +19,17 @@ func (c *CronContext) GetContextId() string {
return c.contextId
}
// GetInterval returns the current cron job interval
func (c *CronContext) GetInterval() time.Duration {
return c.cronJobInterval
}
// NewCronJobContext returns a new cron job context
func NewCronJobContext(cronJobName string) *CronContext {
func NewCronJobContext(cronJobName string, cronJobInterval time.Duration) *CronContext {
return &CronContext{
Context: context.Background(),
contextId: generateNewRandomCronContextId(cronJobName),
Context: context.Background(),
contextId: generateNewRandomCronContextId(cronJobName),
cronJobInterval: cronJobInterval,
}
}
+4
View File
@@ -80,6 +80,10 @@ func (c *CronJobSchedulerContainer) registerAllJobs(ctx core.Context, config *se
if config.EnableRemoveExpiredTokens {
Container.registerIntervalJob(ctx, RemoveExpiredTokensJob)
}
if config.EnableCreateScheduledTransaction {
Container.registerIntervalJob(ctx, CreateScheduledTransactionJob)
}
}
func (c *CronJobSchedulerContainer) registerIntervalJob(ctx core.Context, job *CronJob) {
+1 -1
View File
@@ -20,7 +20,7 @@ type CronJob struct {
func (j *CronJob) doRun() {
start := time.Now()
c := core.NewCronJobContext(j.Name)
c := core.NewCronJobContext(j.Name, j.Period.GetInterval())
if duplicatechecker.Container.Current != nil {
localAddr, err := utils.GetLocalIPAddressesString()
+16
View File
@@ -1,6 +1,7 @@
package cron
import (
"fmt"
"time"
"github.com/go-co-op/gocron/v2"
@@ -22,6 +23,11 @@ type CronJobFixedHourPeriod struct {
Hour uint32
}
// CronJobEvery15MinutesPeriod represents the period of execution at every 15 minutes
type CronJobEvery15MinutesPeriod struct {
Second uint32
}
// CronJobFixedTimePeriod represents the period of execution at fixed time
type CronJobFixedTimePeriod struct {
Time time.Time
@@ -52,6 +58,16 @@ func (p CronJobFixedHourPeriod) ToJobDefinition() gocron.JobDefinition {
)
}
// GetInterval returns the interval time of the period of CronJobEvery15MinutesPeriod
func (p CronJobEvery15MinutesPeriod) GetInterval() time.Duration {
return 15 * time.Minute
}
// ToJobDefinition returns the gocron job definition of the period of CronJobEvery15MinutesPeriod
func (p CronJobEvery15MinutesPeriod) ToJobDefinition() gocron.JobDefinition {
return gocron.CronJob(fmt.Sprintf("%d */15 * * * *", p.Second), true)
}
// GetInterval returns the interval time of the period of CronJobFixedTimePeriod
func (p CronJobFixedTimePeriod) GetInterval() time.Duration {
return 0
+55
View File
@@ -97,6 +97,61 @@ func TestCronJobNextRunTimeWithFixedHourPeriod(t *testing.T) {
assert.Nil(t, err)
}
func TestCronJobNextRunTimeWithEvery15MinutesPeriod(t *testing.T) {
scheduler, err := gocron.NewScheduler(
gocron.WithLocation(time.Local),
)
assert.Nil(t, err)
expectedSecond := uint32(23)
job := CronJob{
Name: "TestCronJobWithEvery15MinutesPeriod",
Description: "The test cron job",
Period: CronJobEvery15MinutesPeriod{
Second: expectedSecond,
},
Run: func(c *core.CronContext) error {
return nil
},
}
assert.Equal(t, 15*time.Minute, job.Period.GetInterval())
gocronJob, err := scheduler.NewJob(
job.Period.ToJobDefinition(),
gocron.NewTask(job.doRun),
gocron.WithName(job.Name),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)
assert.Nil(t, err)
scheduler.Start()
nextRunTime, err := gocronJob.NextRun()
assert.Nil(t, err)
nextMinuteTime := time.Now()
if (nextMinuteTime.Minute() == 0 || nextMinuteTime.Minute() == 15 || nextMinuteTime.Minute() == 30 || nextMinuteTime.Minute() == 45) && nextMinuteTime.Second() < int(expectedSecond) {
// Do Nothing
} else {
nextMinute := ((nextMinuteTime.Minute() / 15) + 1) * 15
minuteDiff := nextMinute - nextMinuteTime.Minute()
nextMinuteTime = nextMinuteTime.Add(time.Duration(int64(minuteDiff) * int64(time.Minute)))
}
assert.Equal(t, nextMinuteTime.Year(), nextRunTime.Year())
assert.Equal(t, nextMinuteTime.Month(), nextRunTime.Month())
assert.Equal(t, nextMinuteTime.Day(), nextRunTime.Day())
assert.Equal(t, nextMinuteTime.Hour(), nextRunTime.Hour())
assert.Equal(t, nextMinuteTime.Minute(), nextRunTime.Minute())
assert.Equal(t, int(expectedSecond), nextRunTime.Second())
err = scheduler.Shutdown()
assert.Nil(t, err)
}
func TestCronJobNextRunTimeWithFixedTimePeriod(t *testing.T) {
scheduler, err := gocron.NewScheduler(
gocron.WithLocation(time.Local),
+14
View File
@@ -1,6 +1,8 @@
package cron
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/services"
)
@@ -16,3 +18,15 @@ var RemoveExpiredTokensJob = &CronJob{
return services.Tokens.DeleteAllExpiredTokens(c)
},
}
// CreateScheduledTransactionJob represents the cron job which periodically create transaction by scheduled transaction template
var CreateScheduledTransactionJob = &CronJob{
Name: "CreateScheduledTransaction",
Description: "Periodically create transaction by scheduled transaction template.",
Period: CronJobEvery15MinutesPeriod{
Second: 0,
},
Run: func(c *core.CronContext) error {
return services.Transactions.CreateScheduledTransactions(c, time.Now().Unix(), c.GetInterval())
},
}
+5 -3
View File
@@ -4,7 +4,9 @@ import "net/http"
// Error codes related to transaction templates
var (
ErrTransactionTemplateIdInvalid = NewNormalError(NormalSubcategoryTemplate, 0, http.StatusBadRequest, "transaction template id is invalid")
ErrTransactionTemplateNotFound = NewNormalError(NormalSubcategoryTemplate, 1, http.StatusBadRequest, "transaction template not found")
ErrTransactionTemplateTypeInvalid = NewNormalError(NormalSubcategoryTemplate, 2, http.StatusBadRequest, "transaction template type is invalid")
ErrTransactionTemplateIdInvalid = NewNormalError(NormalSubcategoryTemplate, 0, http.StatusBadRequest, "transaction template id is invalid")
ErrTransactionTemplateNotFound = NewNormalError(NormalSubcategoryTemplate, 1, http.StatusBadRequest, "transaction template not found")
ErrTransactionTemplateTypeInvalid = NewNormalError(NormalSubcategoryTemplate, 2, http.StatusBadRequest, "transaction template type is invalid")
ErrScheduledTransactionNotEnabled = NewNormalError(NormalSubcategoryTemplate, 3, http.StatusBadRequest, "scheduled transaction is not enabled")
ErrScheduledTransactionFrequencyInvalid = NewNormalError(NormalSubcategoryTemplate, 4, http.StatusBadRequest, "scheduled transaction frequency is invalid")
)
@@ -19,6 +19,7 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc {
buildBooleanSetting("r", config.EnableUserRegister),
buildBooleanSetting("f", config.EnableUserForgetPassword),
buildBooleanSetting("v", config.EnableUserVerifyEmail),
buildBooleanSetting("s", config.EnableScheduledTransaction),
buildBooleanSetting("e", config.EnableDataExport),
buildStringSetting("m", strings.Replace(config.MapProvider, "_", "-", -1)),
}
+6 -5
View File
@@ -7,9 +7,10 @@ type ClearDataRequest struct {
// DataStatisticsResponse represents a view-object of user data statistic
type DataStatisticsResponse struct {
TotalAccountCount int64 `json:"totalAccountCount,string"`
TotalTransactionCategoryCount int64 `json:"totalTransactionCategoryCount,string"`
TotalTransactionTagCount int64 `json:"totalTransactionTagCount,string"`
TotalTransactionCount int64 `json:"totalTransactionCount,string"`
TotalTransactionTemplateCount int64 `json:"totalTransactionTemplateCount,string"`
TotalAccountCount int64 `json:"totalAccountCount,string"`
TotalTransactionCategoryCount int64 `json:"totalTransactionCategoryCount,string"`
TotalTransactionTagCount int64 `json:"totalTransactionTagCount,string"`
TotalTransactionCount int64 `json:"totalTransactionCount,string"`
TotalTransactionTemplateCount int64 `json:"totalTransactionTemplateCount,string"`
TotalScheduledTransactionCount int64 `json:"totalScheduledTransactionCount,string"`
}
+1
View File
@@ -49,6 +49,7 @@ type Transaction struct {
GeoLongitude float64 `xorm:"INDEX(IDX_transaction_uid_deleted_time_longitude_latitude)"`
GeoLatitude float64 `xorm:"INDEX(IDX_transaction_uid_deleted_time_longitude_latitude)"`
CreatedIp string `xorm:"VARCHAR(39)"`
ScheduledCreated bool
CreatedUnixTime int64
UpdatedUnixTime int64
DeletedUnixTime int64
+110 -60
View File
@@ -11,30 +11,45 @@ type TransactionTemplateType byte
// Transaction template types
const (
TRANSACTION_TEMPLATE_TYPE_NORMAL TransactionTemplateType = 1
TRANSACTION_TEMPLATE_TYPE_NORMAL TransactionTemplateType = 1
TRANSACTION_TEMPLATE_TYPE_SCHEDULE TransactionTemplateType = 2
)
// TransactionScheduleFrequencyType represents transaction template schedule frequency type
type TransactionScheduleFrequencyType byte
// Transaction template schedule frequency types
const (
TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED TransactionScheduleFrequencyType = 0
TRANSACTION_SCHEDULE_FREQUENCY_TYPE_WEEKLY TransactionScheduleFrequencyType = 1
TRANSACTION_SCHEDULE_FREQUENCY_TYPE_MONTHLY TransactionScheduleFrequencyType = 2
)
// TransactionTemplate represents transaction template stored in database
type TransactionTemplate struct {
TemplateId int64 `xorm:"PK"`
Uid int64 `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
Deleted bool `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
TemplateType TransactionTemplateType `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
Name string `xorm:"VARCHAR(32) NOT NULL"`
Type TransactionType `xorm:"NOT NULL"`
CategoryId int64 `xorm:"NOT NULL"`
AccountId int64 `xorm:"NOT NULL"`
TagIds string `xorm:"VARCHAR(255) NOT NULL"`
Amount int64 `xorm:"NOT NULL"`
RelatedAccountId int64 `xorm:"NOT NULL"`
RelatedAccountAmount int64 `xorm:"NOT NULL"`
HideAmount bool `xorm:"NOT NULL"`
Comment string `xorm:"VARCHAR(255) NOT NULL"`
DisplayOrder int32 `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
Hidden bool `xorm:"NOT NULL"`
CreatedUnixTime int64
UpdatedUnixTime int64
DeletedUnixTime int64
TemplateId int64 `xorm:"PK"`
Uid int64 `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) NOT NULL"`
Deleted bool `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at) NOT NULL"`
TemplateType TransactionTemplateType `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at) NOT NULL"`
Name string `xorm:"VARCHAR(32) NOT NULL"`
Type TransactionType `xorm:"NOT NULL"`
CategoryId int64 `xorm:"NOT NULL"`
AccountId int64 `xorm:"NOT NULL"`
ScheduledFrequencyType TransactionScheduleFrequencyType `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at)"`
ScheduledFrequency string `xorm:"VARCHAR(100)"`
ScheduledAt int16 `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at)"`
ScheduledTimezoneUtcOffset int16
TagIds string `xorm:"VARCHAR(255) NOT NULL"`
Amount int64 `xorm:"NOT NULL"`
RelatedAccountId int64 `xorm:"NOT NULL"`
RelatedAccountAmount int64 `xorm:"NOT NULL"`
HideAmount bool `xorm:"NOT NULL"`
Comment string `xorm:"VARCHAR(255) NOT NULL"`
DisplayOrder int32 `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) NOT NULL"`
Hidden bool `xorm:"NOT NULL"`
CreatedUnixTime int64
UpdatedUnixTime int64
DeletedUnixTime int64
}
// TransactionTemplateListRequest represents all parameters of transaction template list request
@@ -49,18 +64,21 @@ type TransactionTemplateGetRequest struct {
// TransactionTemplateCreateRequest represents all parameters of transaction template creation request
type TransactionTemplateCreateRequest struct {
TemplateType TransactionTemplateType `json:"templateType"`
Name string `json:"name" binding:"required,notBlank,max=32"`
Type TransactionType `json:"type" binding:"required"`
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
ClientSessionId string `json:"clientSessionId"`
TemplateType TransactionTemplateType `json:"templateType"`
Name string `json:"name" binding:"required,notBlank,max=32"`
Type TransactionType `json:"type" binding:"required"`
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType" binding:"omitempty"`
ScheduledFrequency *string `json:"scheduledFrequency" binding:"omitempty"`
ScheduledTimezoneUtcOffset *int16 `json:"utcOffset" binding:"omitempty,min=-720,max=840"`
ClientSessionId string `json:"clientSessionId"`
}
// TransactionTemplateModifyNameRequest represents all parameters of transaction template name modification request
@@ -71,17 +89,20 @@ type TransactionTemplateModifyNameRequest struct {
// TransactionTemplateModifyRequest represents all parameters of transaction template modification request
type TransactionTemplateModifyRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
Name string `json:"name" binding:"required,notBlank,max=32"`
Type TransactionType `json:"type" binding:"required"`
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
Id int64 `json:"id,string" binding:"required,min=1"`
Name string `json:"name" binding:"required,notBlank,max=32"`
Type TransactionType `json:"type" binding:"required"`
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType" binding:"omitempty"`
ScheduledFrequency *string `json:"scheduledFrequency" binding:"omitempty"`
ScheduledTimezoneUtcOffset *int16 `json:"utcOffset" binding:"omitempty,min=-720,max=840"`
}
// TransactionTemplateHideRequest represents all parameters of transaction template hiding request
@@ -108,14 +129,54 @@ type TransactionTemplateDeleteRequest struct {
type TransactionTemplateInfoResponse struct {
*TransactionInfoResponse
TemplateType TransactionTemplateType `json:"templateType"`
Name string `json:"name"`
DisplayOrder int32 `json:"displayOrder"`
Hidden bool `json:"hidden"`
TemplateType TransactionTemplateType `json:"templateType"`
Name string `json:"name"`
ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType,omitempty"`
ScheduledFrequency *string `json:"scheduledFrequency,omitempty"`
ScheduledAt *int16 `json:"scheduledAt,omitempty"`
DisplayOrder int32 `json:"displayOrder"`
Hidden bool `json:"hidden"`
}
// ToTransactionInfoResponse returns a view-object according to database model
func (t *TransactionTemplate) ToTransactionInfoResponse(utcOffset int16) *TransactionInfoResponse {
// GetTagIds returns all tag ids of the transaction template
func (t *TransactionTemplate) GetTagIds() []int64 {
tagIds := make([]string, 0)
if t.TagIds != "" {
tagIds = strings.Split(t.TagIds, ",")
}
result, _ := utils.StringArrayToInt64Array(tagIds)
return result
}
// ToTransactionTemplateInfoResponse returns a view-object according to database model
func (t *TransactionTemplate) ToTransactionTemplateInfoResponse(serverUtcOffset int16) *TransactionTemplateInfoResponse {
utcOffset := serverUtcOffset
if t.TemplateType == TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
utcOffset = t.ScheduledTimezoneUtcOffset
}
response := &TransactionTemplateInfoResponse{
TransactionInfoResponse: t.toTransactionInfoResponse(utcOffset),
TemplateType: t.TemplateType,
Name: t.Name,
DisplayOrder: t.DisplayOrder,
Hidden: t.Hidden,
}
if t.TemplateType == TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
response.ScheduledFrequencyType = &t.ScheduledFrequencyType
response.ScheduledFrequency = &t.ScheduledFrequency
response.ScheduledAt = &t.ScheduledAt
}
return response
}
func (t *TransactionTemplate) toTransactionInfoResponse(utcOffset int16) *TransactionInfoResponse {
tagIds := make([]string, 0, 0)
if t.TagIds != "" {
@@ -141,17 +202,6 @@ func (t *TransactionTemplate) ToTransactionInfoResponse(utcOffset int16) *Transa
}
}
// ToTransactionTemplateInfoResponse returns a view-object according to database model
func (t *TransactionTemplate) ToTransactionTemplateInfoResponse(utcOffset int16) *TransactionTemplateInfoResponse {
return &TransactionTemplateInfoResponse{
TransactionInfoResponse: t.ToTransactionInfoResponse(utcOffset),
TemplateType: t.TemplateType,
Name: t.Name,
DisplayOrder: t.DisplayOrder,
Hidden: t.Hidden,
}
}
// TransactionTemplateInfoResponseSlice represents the slice data structure of TransactionTemplateInfoResponse
type TransactionTemplateInfoResponseSlice []*TransactionTemplateInfoResponse
+11 -1
View File
@@ -26,7 +26,7 @@ func (s *ServiceUsingDB) TokenDB(uid int64) *datastore.Database {
return s.container.TokenStore.Choose(uid)
}
// TokenDBByIndex returns the datastore by index
// TokenDBByIndex returns the datastore which contains user token by index
func (s *ServiceUsingDB) TokenDBByIndex(index int) *datastore.Database {
return s.container.TokenStore.Get(index)
}
@@ -41,6 +41,16 @@ func (s *ServiceUsingDB) UserDataDB(uid int64) *datastore.Database {
return s.container.UserDataStore.Choose(uid)
}
// UserDataDBByIndex returns the datastore which contains user data by index
func (s *ServiceUsingDB) UserDataDBByIndex(index int) *datastore.Database {
return s.container.UserDataStore.Get(index)
}
// UserDataDBCount returns the count of datastores which contains user data
func (s *ServiceUsingDB) UserDataDBCount() int {
return s.container.UserDataStore.Count()
}
// ServiceUsingConfig represents a service that need to use config
type ServiceUsingConfig struct {
container *settings.ConfigContainer
+13 -2
View File
@@ -29,7 +29,7 @@ var (
}
)
// GetTotalNormalTemplateCountByUid returns total template count of user
// GetTotalNormalTemplateCountByUid returns total normal template count of user
func (s *TransactionTemplateService) GetTotalNormalTemplateCountByUid(c core.Context, uid int64) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
@@ -40,6 +40,17 @@ func (s *TransactionTemplateService) GetTotalNormalTemplateCountByUid(c core.Con
return count, err
}
// GetTotalScheduledTemplateCountByUid returns total scheduled transaction count of user
func (s *TransactionTemplateService) GetTotalScheduledTemplateCountByUid(c core.Context, uid int64) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
count, err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=? AND template_type=?", uid, false, models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE).Count(&models.TransactionTemplate{})
return count, err
}
// GetAllTemplatesByUid returns all transaction template models of user
func (s *TransactionTemplateService) GetAllTemplatesByUid(c core.Context, uid int64, templateType models.TransactionTemplateType) ([]*models.TransactionTemplate, error) {
if uid <= 0 {
@@ -125,7 +136,7 @@ func (s *TransactionTemplateService) ModifyTemplate(c core.Context, template *mo
template.UpdatedUnixTime = time.Now().Unix()
return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.ID(template.TemplateId).Cols("name", "type", "category_id", "account_id", "tag_ids", "amount", "related_account_id", "related_account_amount", "hide_amount", "comment", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template)
updatedRows, err := sess.ID(template.TemplateId).Cols("name", "type", "category_id", "account_id", "scheduled_frequency_type", "scheduled_frequency", "scheduled_at", "scheduled_timezone_utc_offset", "tag_ids", "amount", "related_account_id", "related_account_amount", "hide_amount", "comment", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template)
if err != nil {
return err
+130
View File
@@ -11,6 +11,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/uuid"
@@ -429,6 +430,135 @@ func (s *TransactionService) CreateTransaction(c core.Context, transaction *mode
})
}
// CreateScheduledTransactions saves all scheduled transactions that should be created now
func (s *TransactionService) CreateScheduledTransactions(c core.Context, currentUnixTime int64, interval time.Duration) error {
var allTemplates []*models.TransactionTemplate
intervalMinute := int(interval / time.Minute)
currentTime := time.Unix(currentUnixTime, 0)
currentMinute := (currentTime.Minute() / intervalMinute) * intervalMinute
startTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), currentTime.Hour(), currentMinute, 0, 0, time.Local)
startTimeInUTC := startTime.In(time.UTC)
minutesElapsedOfDayInUtc := startTimeInUTC.Hour()*60 + startTimeInUTC.Minute()
secondsElapsedOfDayInUtc := minutesElapsedOfDayInUtc * 60
todayFirstTimeInUTC := startTimeInUTC.Add(time.Duration(-secondsElapsedOfDayInUtc) * time.Second)
todayFirstUnixTimeInUTC := todayFirstTimeInUTC.Unix()
minScheduledAt := minutesElapsedOfDayInUtc
maxScheduledAt := minScheduledAt + intervalMinute
for i := 0; i < s.UserDataDBCount(); i++ {
var templates []*models.TransactionTemplate
err := s.UserDataDBByIndex(i).NewSession(c).Where("deleted=? AND template_type=? AND (scheduled_frequency_type=? OR scheduled_frequency_type=?) AND scheduled_at>=? AND scheduled_at<?", false, models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE, models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_WEEKLY, models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_MONTHLY, minScheduledAt, maxScheduledAt).Find(&templates)
if err != nil {
return err
}
allTemplates = append(allTemplates, templates...)
}
if len(allTemplates) < 1 {
return nil
}
log.Infof(c, "[transactions.CreateScheduledTransactions] should process %d scheduled transaction templates now (scheduled at from %d to %d)", len(allTemplates), minScheduledAt, maxScheduledAt)
successCount := 0
skipCount := 0
failedCount := 0
for i := 0; i < len(allTemplates); i++ {
template := allTemplates[i]
if template.ScheduledFrequencyType == models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED {
skipCount++
log.Warnf(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" disabled scheduled transaction frequency", template.TemplateId)
continue
}
if (template.ScheduledFrequencyType != models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_WEEKLY &&
template.ScheduledFrequencyType != models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_MONTHLY) ||
template.ScheduledFrequency == "" {
skipCount++
log.Warnf(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" has invalid scheduled transaction frequency", template.TemplateId)
continue
}
frequencyValues, err := utils.StringArrayToInt64Array(strings.Split(template.ScheduledFrequency, ","))
if err != nil {
skipCount++
log.Warnf(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" has invalid scheduled transaction frequency, because %s", template.TemplateId, err.Error())
continue
}
frequencyValueSet := utils.ToSet(frequencyValues)
templateTimeZone := time.FixedZone("Template Timezone", int(template.ScheduledTimezoneUtcOffset)*60)
transactionUnixTime := todayFirstUnixTimeInUTC + int64(template.ScheduledAt)*60
transactionTime := time.Unix(transactionUnixTime, 0).In(templateTimeZone)
if template.ScheduledFrequencyType == models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_WEEKLY && !frequencyValueSet[int64(transactionTime.Weekday())] {
skipCount++
log.Infof(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" does not need to create transaction, today is %s", template.TemplateId, startTimeInUTC.Weekday())
continue
} else if template.ScheduledFrequencyType == models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_MONTHLY && !frequencyValueSet[int64(transactionTime.Day())] {
skipCount++
log.Infof(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" does not need to create transaction, today is %d of month", template.TemplateId, startTimeInUTC.Day())
continue
}
var transactionDbType models.TransactionDbType
if template.Type == models.TRANSACTION_TYPE_EXPENSE {
transactionDbType = models.TRANSACTION_DB_TYPE_EXPENSE
} else if template.Type == models.TRANSACTION_TYPE_INCOME {
transactionDbType = models.TRANSACTION_DB_TYPE_INCOME
} else if template.Type == models.TRANSACTION_TYPE_TRANSFER {
transactionDbType = models.TRANSACTION_DB_TYPE_TRANSFER_OUT
} else {
skipCount++
log.Warnf(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" has invalid transaction type", template.TemplateId)
continue
}
transaction := &models.Transaction{
Uid: template.Uid,
Type: transactionDbType,
CategoryId: template.CategoryId,
TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()),
TimezoneUtcOffset: template.ScheduledTimezoneUtcOffset,
AccountId: template.AccountId,
Amount: template.Amount,
HideAmount: template.HideAmount,
Comment: template.Comment,
CreatedIp: "127.0.0.1",
ScheduledCreated: true,
}
if template.Type == models.TRANSACTION_TYPE_TRANSFER {
transaction.RelatedAccountId = template.RelatedAccountId
transaction.RelatedAccountAmount = template.RelatedAccountAmount
}
tagIds := template.GetTagIds()
err = s.CreateTransaction(c, transaction, tagIds)
if err == nil {
successCount++
log.Infof(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" has created a new trasaction \"id:%d\"", template.TemplateId, transaction.TransactionId)
} else {
failedCount++
log.Errorf(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" failed to create new trasaction", template.TemplateId)
}
}
log.Infof(c, "[transactions.CreateScheduledTransactions] %d transactions has been created successfully, %d templates does not need to create transactions and %d transactions failed to create", successCount, skipCount, failedCount)
return nil
}
// ModifyTransaction saves an existed transaction to database
func (s *TransactionService) ModifyTransaction(c core.Context, transaction *models.Transaction, currentTagIdsCount int, addTagIds []int64, removeTagIds []int64) error {
if transaction.Uid <= 0 {
+4
View File
@@ -248,6 +248,7 @@ type Config struct {
// Cron
EnableRemoveExpiredTokens bool
EnableCreateScheduledTransaction bool
// Secret
SecretKeyNoSet bool
@@ -270,6 +271,7 @@ type Config struct {
EnableUserForceVerifyEmail bool
EnableUserForgetPassword bool
ForgetPasswordRequireVerifyEmail bool
EnableScheduledTransaction bool
AvatarProvider core.UserAvatarProviderType
// Data
@@ -679,6 +681,7 @@ func loadDuplicateCheckerConfiguration(config *Config, configFile *ini.File, sec
func loadCronConfiguration(config *Config, configFile *ini.File, sectionName string) error {
config.EnableRemoveExpiredTokens = getConfigItemBoolValue(configFile, sectionName, "enable_remove_expired_tokens", false)
config.EnableCreateScheduledTransaction = getConfigItemBoolValue(configFile, sectionName, "enable_create_scheduled_transaction", false)
return nil
}
@@ -737,6 +740,7 @@ func loadUserConfiguration(config *Config, configFile *ini.File, sectionName str
config.EnableUserForceVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "enable_force_email_verify", false)
config.EnableUserForgetPassword = getConfigItemBoolValue(configFile, sectionName, "enable_forget_password", false)
config.ForgetPasswordRequireVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "forget_password_require_email_verify", false)
config.EnableScheduledTransaction = getConfigItemBoolValue(configFile, sectionName, "enable_scheduled_transaction", false)
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == string(core.USER_AVATAR_PROVIDER_INTERNAL) {
config.AvatarProvider = core.USER_AVATAR_PROVIDER_INTERNAL
+12
View File
@@ -57,3 +57,15 @@ func ToUniqueInt64Slice(items []int64) []int64 {
return uniqueItems
}
// ToSet returns a map where the keys are the items in the specified array
func ToSet(items []int64) map[int64]bool {
itemExistMap := make(map[int64]bool)
for i := 0; i < len(items); i++ {
item := items[i]
itemExistMap[item] = true
}
return itemExistMap
}
+22
View File
@@ -130,3 +130,25 @@ func TestToUniqueInt64Slice_NilOrEmpty(t *testing.T) {
actualValue = ToUniqueInt64Slice(arr)
assert.Equal(t, expectedValue, actualValue)
}
func TestToSet(t *testing.T) {
arr := []int64{0, 1, 2, 3, 2, 4, 0}
actualValue := ToSet(arr)
assert.Equal(t, 5, len(actualValue))
assert.Equal(t, true, actualValue[0])
assert.Equal(t, true, actualValue[1])
assert.Equal(t, true, actualValue[2])
assert.Equal(t, true, actualValue[3])
assert.Equal(t, true, actualValue[4])
assert.Equal(t, false, actualValue[5])
}
func TestToSet_NilOrEmpty(t *testing.T) {
var arr []int64 = nil
actualValue := ToSet(arr)
assert.Equal(t, 0, len(actualValue))
arr = []int64{}
actualValue = ToSet(arr)
assert.Equal(t, 0, len(actualValue))
}