mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-14 06:57:35 +08:00
support scheduled transaction (#2)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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())
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user