aboutsummaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--internal/chore/handler.go2
-rw-r--r--internal/chore/model/model.go18
-rw-r--r--internal/chore/scheduler.go30
-rw-r--r--internal/chore/scheduler_test.go229
-rw-r--r--internal/user/handler.go27
-rw-r--r--internal/user/model/model.go24
-rw-r--r--internal/user/repo/repository.go4
7 files changed, 317 insertions, 17 deletions
diff --git a/internal/chore/handler.go b/internal/chore/handler.go
index 16c80ef..01490cc 100644
--- a/internal/chore/handler.go
+++ b/internal/chore/handler.go
@@ -33,7 +33,7 @@ type ThingTrigger struct {
type ChoreReq struct {
Name string `json:"name" binding:"required"`
- FrequencyType string `json:"frequencyType"`
+ FrequencyType chModel.FrequencyType `json:"frequencyType"`
ID int `json:"id"`
DueDate string `json:"dueDate"`
Assignees []chModel.ChoreAssignees `json:"assignees"`
diff --git a/internal/chore/model/model.go b/internal/chore/model/model.go
index 38d8354..b2633b3 100644
--- a/internal/chore/model/model.go
+++ b/internal/chore/model/model.go
@@ -6,10 +6,26 @@ import (
tModel "donetick.com/core/internal/thing/model"
)
+type FrequencyType string
+
+const (
+ FrequancyTypeOnce FrequencyType = "once"
+ FrequancyTypeDaily FrequencyType = "daily"
+ FrequancyTypeWeekly FrequencyType = "weekly"
+ FrequancyTypeMonthly FrequencyType = "monthly"
+ FrequancyTypeYearly FrequencyType = "yearly"
+ FrequancyTypeAdaptive FrequencyType = "adaptive"
+ FrequancyTypeIntervel FrequencyType = "interval"
+ FrequancyTypeDayOfTheWeek FrequencyType = "days_of_the_week"
+ FrequancyTypeDayOfTheMonth FrequencyType = "day_of_the_month"
+ FrequancyTypeTrigger FrequencyType = "trigger"
+ FrequancyTypeNoRepeat FrequencyType = "no_repeat"
+)
+
type Chore struct {
ID int `json:"id" gorm:"primary_key"`
Name string `json:"name" gorm:"column:name"` // Chore description
- FrequencyType string `json:"frequencyType" gorm:"column:frequency_type"` // "daily", "weekly", "monthly", "yearly", "adaptive",or "custom"
+ FrequencyType FrequencyType `json:"frequencyType" gorm:"column:frequency_type"` // "daily", "weekly", "monthly", "yearly", "adaptive",or "custom"
Frequency int `json:"frequency" gorm:"column:frequency"` // Number of days, weeks, months, or years between chores
FrequencyMetadata *string `json:"frequencyMetadata" gorm:"column:frequency_meta"` // Additional frequency information
NextDueDate *time.Time `json:"nextDueDate" gorm:"column:next_due_date;index"` // When the chore is due
diff --git a/internal/chore/scheduler.go b/internal/chore/scheduler.go
index 5413447..cec120d 100644
--- a/internal/chore/scheduler.go
+++ b/internal/chore/scheduler.go
@@ -3,6 +3,7 @@ package chore
import (
"encoding/json"
"fmt"
+ "math"
"math/rand"
"strings"
"time"
@@ -141,11 +142,15 @@ func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.T
return &nextDueDate, nil
}
-
func scheduleAdaptiveNextDueDate(chore *chModel.Chore, completedDate time.Time, history []*chModel.ChoreHistory) (*time.Time, error) {
- // will generate due date base on history and the different between the completed date and the due date
- // the more recent the higher weight
- if len(history) <= 1 {
+
+ history = append([]*chModel.ChoreHistory{
+ {
+ CompletedAt: &completedDate,
+ },
+ }, history...)
+
+ if len(history) < 2 {
if chore.NextDueDate != nil {
diff := completedDate.UTC().Sub(chore.NextDueDate.UTC())
nextDueDate := completedDate.UTC().Add(diff)
@@ -153,22 +158,23 @@ func scheduleAdaptiveNextDueDate(chore *chModel.Chore, completedDate time.Time,
}
return nil, nil
}
- var weight float64
+
+ var totalDelay float64
var totalWeight float64
- var nextDueDate time.Time
+ decayFactor := 0.5 // Adjust this value to control the decay rate
+
for i := 0; i < len(history)-1; i++ {
delay := history[i].CompletedAt.UTC().Sub(history[i+1].CompletedAt.UTC()).Seconds()
- weight = delay * float64(len(history)-i)
+ weight := math.Pow(decayFactor, float64(i))
+ totalDelay += delay * weight
totalWeight += weight
}
- // calculate the average delay
- averageDelay := totalWeight / float64(len(history)-1)
- // calculate the difference between the completed date and the due date
- nextDueDate = completedDate.UTC().Add(time.Duration(averageDelay) * time.Second)
+
+ averageDelay := totalDelay / totalWeight
+ nextDueDate := completedDate.UTC().Add(time.Duration(averageDelay) * time.Second)
return &nextDueDate, nil
}
-
func RemoveAssigneeAndReassign(chore *chModel.Chore, userID int) {
for i, assignee := range chore.Assignees {
if assignee.UserID == userID {
diff --git a/internal/chore/scheduler_test.go b/internal/chore/scheduler_test.go
new file mode 100644
index 0000000..21d064a
--- /dev/null
+++ b/internal/chore/scheduler_test.go
@@ -0,0 +1,229 @@
+package chore
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ chModel "donetick.com/core/internal/chore/model"
+)
+
+func TestScheduleNextDueDateBasic(t *testing.T) {
+ choreTime := time.Now()
+ freqencyMetadataBytes := `{"time":"2024-07-07T14:30:00-04:00"}`
+
+ testTable := []struct {
+ chore *chModel.Chore
+ expected time.Time
+ }{
+ {
+ chore: &chModel.Chore{
+ FrequencyType: chModel.FrequancyTypeDaily,
+ NextDueDate: &choreTime,
+ FrequencyMetadata: &freqencyMetadataBytes,
+ },
+ expected: choreTime.AddDate(0, 0, 1),
+ },
+ {
+ chore: &chModel.Chore{
+ FrequencyType: chModel.FrequancyTypeWeekly,
+ NextDueDate: &choreTime,
+ FrequencyMetadata: &freqencyMetadataBytes,
+ },
+ expected: choreTime.AddDate(0, 0, 7),
+ },
+ {
+ chore: &chModel.Chore{
+ FrequencyType: chModel.FrequancyTypeMonthly,
+ NextDueDate: &choreTime,
+ FrequencyMetadata: &freqencyMetadataBytes,
+ },
+ expected: choreTime.AddDate(0, 1, 0),
+ },
+ {
+ chore: &chModel.Chore{
+ FrequencyType: chModel.FrequancyTypeYearly,
+ NextDueDate: &choreTime,
+ FrequencyMetadata: &freqencyMetadataBytes,
+ },
+ expected: choreTime.AddDate(1, 0, 0),
+ },
+
+ //
+ }
+ for _, tt := range testTable {
+ t.Run(string(tt.chore.FrequencyType), func(t *testing.T) {
+
+ actual, err := scheduleNextDueDate(tt.chore, choreTime)
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ if actual != nil && actual.UTC().Format(time.RFC3339) != tt.expected.UTC().Format(time.RFC3339) {
+ t.Errorf("Expected: %v, Actual: %v", tt.expected, actual)
+ }
+ })
+ }
+}
+
+func TestScheduleNextDueDateDayOfTheWeek(t *testing.T) {
+ choreTime := time.Now()
+
+ Monday := "monday"
+ Wednesday := "wednesday"
+
+ timeOfChore := "2024-07-07T16:30:00-04:00"
+ getExpectedTime := func(choreTime time.Time, timeOfChore string) time.Time {
+ t, err := time.Parse(time.RFC3339, timeOfChore)
+ if err != nil {
+ return time.Time{}
+ }
+ return time.Date(choreTime.Year(), choreTime.Month(), choreTime.Day(), t.Hour(), t.Minute(), 0, 0, t.Location())
+ }
+ nextSaturday := choreTime.AddDate(0, 0, 1)
+ for nextSaturday.Weekday() != time.Saturday {
+ nextSaturday = nextSaturday.AddDate(0, 0, 1)
+ }
+
+ nextMonday := choreTime.AddDate(0, 0, 1)
+ for nextMonday.Weekday() != time.Monday {
+ nextMonday = nextMonday.AddDate(0, 0, 1)
+ }
+
+ nextTuesday := choreTime.AddDate(0, 0, 1)
+ for nextTuesday.Weekday() != time.Tuesday {
+ nextTuesday = nextTuesday.AddDate(0, 0, 1)
+ }
+
+ nextWednesday := choreTime.AddDate(0, 0, 1)
+ for nextWednesday.Weekday() != time.Wednesday {
+ nextWednesday = nextWednesday.AddDate(0, 0, 1)
+ }
+
+ nextThursday := choreTime.AddDate(0, 0, 1)
+ for nextThursday.Weekday() != time.Thursday {
+ nextThursday = nextThursday.AddDate(0, 0, 1)
+ }
+
+ testTable := []struct {
+ chore *chModel.Chore
+ frequencyMetadata *chModel.FrequencyMetadata
+ expected time.Time
+ }{
+ {
+ chore: &chModel.Chore{
+ FrequencyType: chModel.FrequancyTypeDayOfTheWeek,
+ NextDueDate: &nextSaturday,
+ },
+ frequencyMetadata: &chModel.FrequencyMetadata{
+ Time: timeOfChore,
+ Days: []*string{&Monday, &Wednesday},
+ },
+
+ expected: getExpectedTime(nextMonday, timeOfChore),
+ },
+ {
+ chore: &chModel.Chore{
+ FrequencyType: chModel.FrequancyTypeDayOfTheWeek,
+ NextDueDate: &nextMonday,
+ },
+ frequencyMetadata: &chModel.FrequencyMetadata{
+ Time: timeOfChore,
+ Days: []*string{&Monday, &Wednesday},
+ },
+ expected: getExpectedTime(nextWednesday, timeOfChore),
+ },
+ }
+ for _, tt := range testTable {
+ t.Run(string(tt.chore.FrequencyType), func(t *testing.T) {
+ bytesFrequencyMetadata, err := json.Marshal(tt.frequencyMetadata)
+ stringFrequencyMetadata := string(bytesFrequencyMetadata)
+
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ tt.chore.FrequencyMetadata = &stringFrequencyMetadata
+ actual, err := scheduleNextDueDate(tt.chore, choreTime)
+
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ if actual != nil && actual.UTC().Format(time.RFC3339) != tt.expected.UTC().Format(time.RFC3339) {
+ t.Errorf("Expected: %v, Actual: %v", tt.expected, actual)
+ }
+ })
+ }
+}
+func TestScheduleAdaptiveNextDueDate(t *testing.T) {
+ getTimeFromDate := func(timeOfChore string) *time.Time {
+ t, err := time.Parse(time.RFC3339, timeOfChore)
+ if err != nil {
+ return nil
+ }
+ return &t
+ }
+ testTable := []struct {
+ description string
+ history []*chModel.ChoreHistory
+ chore *chModel.Chore
+ expected *time.Time
+ completeDate *time.Time
+ }{
+ {
+ description: "Every Two days",
+ chore: &chModel.Chore{
+ NextDueDate: getTimeFromDate("2024-07-13T01:30:00-00:00"),
+ },
+ history: []*chModel.ChoreHistory{
+ {
+ CompletedAt: getTimeFromDate("2024-07-11T01:30:00-00:00"),
+ },
+ // {
+ // CompletedAt: getTimeFromDate("2024-07-09T01:30:00-00:00"),
+ // },
+ // {
+ // CompletedAt: getTimeFromDate("2024-07-07T01:30:00-00:00"),
+ // },
+ },
+ expected: getTimeFromDate("2024-07-15T01:30:00-00:00"),
+ },
+ {
+ description: "Every 8 days",
+ chore: &chModel.Chore{
+ NextDueDate: getTimeFromDate("2024-07-13T01:30:00-00:00"),
+ },
+ history: []*chModel.ChoreHistory{
+ {
+ CompletedAt: getTimeFromDate("2024-07-05T01:30:00-00:00"),
+ },
+ {
+ CompletedAt: getTimeFromDate("2024-06-27T01:30:00-00:00"),
+ },
+ },
+ expected: getTimeFromDate("2024-07-21T01:30:00-00:00"),
+ },
+ {
+ description: "40 days with limit Data",
+ chore: &chModel.Chore{
+ NextDueDate: getTimeFromDate("2024-07-13T01:30:00-00:00"),
+ },
+ history: []*chModel.ChoreHistory{
+ {CompletedAt: getTimeFromDate("2024-06-03T01:30:00-00:00")},
+ },
+ expected: getTimeFromDate("2024-08-22T01:30:00-00:00"),
+ },
+ }
+ for _, tt := range testTable {
+ t.Run(tt.description, func(t *testing.T) {
+ expectedNextDueDate := tt.expected
+
+ actualNextDueDate, err := scheduleAdaptiveNextDueDate(tt.chore, *tt.chore.NextDueDate, tt.history)
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+
+ if actualNextDueDate == nil || !actualNextDueDate.Equal(*expectedNextDueDate) {
+ t.Errorf("Expected: %v, Actual: %v", expectedNextDueDate, actualNextDueDate)
+ }
+ })
+ }
+}
diff --git a/internal/user/handler.go b/internal/user/handler.go
index 15e881f..ff885f3 100644
--- a/internal/user/handler.go
+++ b/internal/user/handler.go
@@ -486,6 +486,32 @@ func (h *Handler) DeleteUserToken(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
}
+func (h *Handler) UpdateNotificationTarget(c *gin.Context) {
+ currentUser, ok := auth.CurrentUser(c)
+ if !ok {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get current user"})
+ return
+ }
+
+ type Request struct {
+ Type uModel.UserNotificationType `json:"type" binding:"required"`
+ Token string `json:"token" binding:"required"`
+ }
+
+ var req Request
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
+ return
+ }
+
+ err := h.userRepo.UpdateNotificationTarget(c, currentUser.ID, req.Token, req.Type)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update notification target"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{})
+}
func Routes(router *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware, limiter *limiter.Limiter) {
userRoutes := router.Group("users")
@@ -497,6 +523,7 @@ func Routes(router *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware, limiter
userRoutes.POST("/tokens", h.CreateLongLivedToken)
userRoutes.GET("/tokens", h.GetAllUserToken)
userRoutes.DELETE("/tokens/:id", h.DeleteUserToken)
+ userRoutes.PUT("/targets", h.UpdateNotificationTarget)
}
authRoutes := router.Group("auth")
diff --git a/internal/user/model/model.go b/internal/user/model/model.go
index 4874ac1..4cfb38b 100644
--- a/internal/user/model/model.go
+++ b/internal/user/model/model.go
@@ -16,9 +16,10 @@ type User struct {
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"` // Updated at
Disabled bool `json:"disabled" gorm:"column:disabled"` // Disabled
// Email string `json:"email" gorm:"column:email"` // Email
- CustomerID *string `gorm:"column:customer_id;<-:false"` // read one column
- Subscription *string `json:"subscription" gorm:"column:subscription;<-:false"` // read one column
- Expiration *string `json:"expiration" gorm:"column:expiration;<-:false"` // read one column
+ CustomerID *string `gorm:"column:customer_id;<-:false"` // read only column
+ Subscription *string `json:"subscription" gorm:"column:subscription;<-:false"` // read only column
+ Expiration *string `json:"expiration" gorm:"column:expiration;<-:false"` // read only column
+ UserNotificationTargets []UserNotificationTarget `json:"-" gorm:"foreignKey:UserID;references:ID"`
}
type UserPasswordReset struct {
@@ -36,3 +37,20 @@ type APIToken struct {
Token string `json:"token" gorm:"column:token;index"` // Index on token
CreatedAt time.Time `json:"createdAt" gorm:"column:created_at"`
}
+
+type UserNotificationTarget struct {
+ ID int `json:"id" gorm:"primary_key"` // Unique identifier
+ UserID int `json:"userId" gorm:"column:user_id;index"` // Index on userID
+ Type UserNotificationType `json:"type" gorm:"column:type"` // Type
+ TargetID string `json:"targetId" gorm:"column:target_id"` // Target ID
+ CreatedAt time.Time `json:"createdAt" gorm:"column:created_at"`
+}
+
+type UserNotificationType int8
+
+const (
+ _ UserNotificationType = iota
+ Android
+ IOS
+ Telegram
+)
diff --git a/internal/user/repo/repository.go b/internal/user/repo/repository.go
index 76ddd54..7dde6fb 100644
--- a/internal/user/repo/repository.go
+++ b/internal/user/repo/repository.go
@@ -158,3 +158,7 @@ func (r *UserRepository) GetAllUserTokens(c context.Context, userID int) ([]*uMo
func (r *UserRepository) DeleteAPIToken(c context.Context, userID int, tokenID string) error {
return r.db.WithContext(c).Where("id = ? AND user_id = ?", tokenID, userID).Delete(&uModel.APIToken{}).Error
}
+
+func (r *UserRepository) UpdateNotificationTarget(c context.Context, userID int, targetID string, targetType uModel.UserNotificationType) error {
+ return r.db.WithContext(c).Model(&uModel.UserNotificationTarget{}).Where("user_id = ? AND type = ?", userID, targetType).Update("target_id", targetID).Error
+}