Fix timezone handling in completeChore and correct frequency type constants
Add Tests for scheduler Build on PR and run tests
This commit is contained in:
parent
de24fd5395
commit
40f51ce783
5 changed files with 376 additions and 423 deletions
52
.github/workflows/docker-image-release.yml
vendored
52
.github/workflows/docker-image-release.yml
vendored
|
@ -1,52 +0,0 @@
|
||||||
# name: Build and Push Docker Image
|
|
||||||
|
|
||||||
# on:
|
|
||||||
# push:
|
|
||||||
# branches:
|
|
||||||
# - main
|
|
||||||
|
|
||||||
# jobs:
|
|
||||||
# build:
|
|
||||||
# name: Build and Push Docker Image
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
# steps:
|
|
||||||
# # Checkout the code from the repository
|
|
||||||
# - name: Checkout repository
|
|
||||||
# uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# # # Download the latest release binary from GitHub releases
|
|
||||||
# # - name: Download latest release binary
|
|
||||||
# # run: |
|
|
||||||
# # latest_release=$(curl --silent "https://api.github.com/repos/donetick/donetick/releases/latest" | jq -r '.tag_name')
|
|
||||||
# # curl -L "https://github.com/donetick/donetick/releases/download/${latest_release}/donetick_Linux_x86_64.tar.gz" -o donetick_Linux_x86_64.tar.gz
|
|
||||||
# # tar -xzf donetick_Linux_x86_64.tar.gz
|
|
||||||
# # chmod +x ./donetick
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# # Log in to Docker Hub
|
|
||||||
# - name: Log in to Docker Hub
|
|
||||||
# uses: docker/login-action@v2
|
|
||||||
# with:
|
|
||||||
# username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
# password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
# # # Log in to GitHub Container Registry
|
|
||||||
# # - name: Login to GitHub Container Registry
|
|
||||||
# # uses: docker/login-action@v3.3.0
|
|
||||||
# # with:
|
|
||||||
# # registry: ghcr.io
|
|
||||||
# # username: ${{ github.repository_owner }}
|
|
||||||
# # password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
|
|
||||||
# # Build and tag Docker image
|
|
||||||
# - name: Build Docker image
|
|
||||||
# run: |
|
|
||||||
# docker build -t ${{ secrets.DOCKER_USERNAME }}/donetick:latest .
|
|
||||||
|
|
||||||
# # Push Docker image
|
|
||||||
# - name: Push Docker image
|
|
||||||
# run: |
|
|
||||||
# docker push ${{ secrets.DOCKER_USERNAME }}/donetick:latest
|
|
|
@ -1025,7 +1025,7 @@ func (h *Handler) completeChore(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
nextDueDate, err = scheduleNextDueDate(chore, completedDate)
|
nextDueDate, err = scheduleNextDueDate(chore, completedDate.UTC())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error scheduling next due date: %s", err)
|
log.Printf("Error scheduling next due date: %s", err)
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, gin.H{
|
||||||
|
|
|
@ -17,7 +17,7 @@ const (
|
||||||
FrequencyTypeMonthly FrequencyType = "monthly"
|
FrequencyTypeMonthly FrequencyType = "monthly"
|
||||||
FrequencyTypeYearly FrequencyType = "yearly"
|
FrequencyTypeYearly FrequencyType = "yearly"
|
||||||
FrequencyTypeAdaptive FrequencyType = "adaptive"
|
FrequencyTypeAdaptive FrequencyType = "adaptive"
|
||||||
FrequencyTypeIntervel FrequencyType = "interval"
|
FrequencyTypeInterval FrequencyType = "interval"
|
||||||
FrequencyTypeDayOfTheWeek FrequencyType = "days_of_the_week"
|
FrequencyTypeDayOfTheWeek FrequencyType = "days_of_the_week"
|
||||||
FrequencyTypeDayOfTheMonth FrequencyType = "day_of_the_month"
|
FrequencyTypeDayOfTheMonth FrequencyType = "day_of_the_month"
|
||||||
FrequencyTypeTrigger FrequencyType = "trigger"
|
FrequencyTypeTrigger FrequencyType = "trigger"
|
||||||
|
|
|
@ -12,18 +12,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.Time, error) {
|
func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.Time, error) {
|
||||||
// if Chore is rolling then the next due date calculated from the completed date, otherwise it's calculated from the due date
|
if chore.FrequencyType == "once" || chore.FrequencyType == "no_repeat" || chore.FrequencyType == "trigger" {
|
||||||
var nextDueDate time.Time
|
|
||||||
var baseDate time.Time
|
|
||||||
var frequencyMetadata chModel.FrequencyMetadata
|
|
||||||
err := json.Unmarshal([]byte(*chore.FrequencyMetadata), &frequencyMetadata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error unmarshalling frequency metadata")
|
|
||||||
}
|
|
||||||
if chore.FrequencyType == "once" {
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var baseDate time.Time
|
||||||
if chore.NextDueDate != nil {
|
if chore.NextDueDate != nil {
|
||||||
// no due date set, use the current date
|
// no due date set, use the current date
|
||||||
baseDate = chore.NextDueDate.UTC()
|
baseDate = chore.NextDueDate.UTC()
|
||||||
|
@ -45,96 +38,110 @@ func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.T
|
||||||
if chore.IsRolling {
|
if chore.IsRolling {
|
||||||
baseDate = completedDate.UTC()
|
baseDate = completedDate.UTC()
|
||||||
}
|
}
|
||||||
if chore.FrequencyType == "daily" {
|
|
||||||
nextDueDate = baseDate.AddDate(0, 0, 1)
|
|
||||||
} else if chore.FrequencyType == "weekly" {
|
|
||||||
nextDueDate = baseDate.AddDate(0, 0, 7)
|
|
||||||
} else if chore.FrequencyType == "monthly" {
|
|
||||||
nextDueDate = baseDate.AddDate(0, 1, 0)
|
|
||||||
} else if chore.FrequencyType == "yearly" {
|
|
||||||
nextDueDate = baseDate.AddDate(1, 0, 0)
|
|
||||||
} else if chore.FrequencyType == "adaptive" {
|
|
||||||
|
|
||||||
// TODO: calculate next due date based on the history of the chore
|
frequencyMetadata := chModel.FrequencyMetadata{}
|
||||||
// calculate the difference between the due date and now in days:
|
err := json.Unmarshal([]byte(*chore.FrequencyMetadata), &frequencyMetadata)
|
||||||
diff := completedDate.UTC().Sub(chore.NextDueDate.UTC())
|
if err != nil {
|
||||||
nextDueDate = completedDate.UTC().Add(diff)
|
return nil, fmt.Errorf("error unmarshalling frequency metadata: %w", err)
|
||||||
} else if chore.FrequencyType == "once" {
|
}
|
||||||
// if the chore is a one-time chore, then the next due date is nil
|
|
||||||
} else if chore.FrequencyType == "interval" {
|
|
||||||
// calculate the difference between the due date and now in days:
|
|
||||||
if *frequencyMetadata.Unit == "hours" {
|
|
||||||
nextDueDate = baseDate.UTC().Add(time.Hour * time.Duration(chore.Frequency))
|
|
||||||
} else if *frequencyMetadata.Unit == "days" {
|
|
||||||
nextDueDate = baseDate.UTC().AddDate(0, 0, chore.Frequency)
|
|
||||||
} else if *frequencyMetadata.Unit == "weeks" {
|
|
||||||
nextDueDate = baseDate.UTC().AddDate(0, 0, chore.Frequency*7)
|
|
||||||
} else if *frequencyMetadata.Unit == "months" {
|
|
||||||
nextDueDate = baseDate.UTC().AddDate(0, chore.Frequency, 0)
|
|
||||||
} else if *frequencyMetadata.Unit == "years" {
|
|
||||||
nextDueDate = baseDate.UTC().AddDate(chore.Frequency, 0, 0)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("invalid frequency unit, cannot calculate next due date")
|
// Handle time-based frequencies, ensure time is in the future
|
||||||
}
|
if chore.FrequencyType == "day_of_the_month" || chore.FrequencyType == "days_of_the_week" || chore.FrequencyType == "interval" {
|
||||||
} else if chore.FrequencyType == "days_of_the_week" {
|
t, err := time.Parse(time.RFC3339, frequencyMetadata.Time)
|
||||||
// TODO : this logic is bad, need to be refactored and be better.
|
|
||||||
// coding at night is almost always bad idea.
|
|
||||||
// calculate the difference between the due date and now in days:
|
|
||||||
var frequencyMetadata chModel.FrequencyMetadata
|
|
||||||
err := json.Unmarshal([]byte(*chore.FrequencyMetadata), &frequencyMetadata)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing time in frequency metadata: %w", err)
|
||||||
return nil, fmt.Errorf("error unmarshalling frequency metadata")
|
|
||||||
}
|
}
|
||||||
//we can only assign to days of the week that part of the frequency metadata.days
|
|
||||||
//it's array of days of the week, for example ["monday", "tuesday", "wednesday"]
|
|
||||||
|
|
||||||
// we need to find the next day of the week in the frequency metadata.days that we can schedule
|
baseDate = time.Date(baseDate.Year(), baseDate.Month(), baseDate.Day(), t.Hour(), t.Minute(), t.Second(), 0, time.UTC)
|
||||||
// if this the last or there is only one. will use same otherwise find the next one:
|
|
||||||
|
|
||||||
// find the index of the chore day in the frequency metadata.days
|
// If the time is in the past today, move it to tomorrow
|
||||||
// loop for next 7 days from the base, if the day in the frequency metadata.days then we can schedule it:
|
if baseDate.Before(completedDate) {
|
||||||
|
baseDate = baseDate.AddDate(0, 0, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch chore.FrequencyType {
|
||||||
|
case "daily":
|
||||||
|
baseDate = baseDate.AddDate(0, 0, 1)
|
||||||
|
case "weekly":
|
||||||
|
baseDate = baseDate.AddDate(0, 0, 7)
|
||||||
|
case "monthly":
|
||||||
|
baseDate = baseDate.AddDate(0, 1, 0)
|
||||||
|
case "yearly":
|
||||||
|
baseDate = baseDate.AddDate(1, 0, 0)
|
||||||
|
case "adaptive":
|
||||||
|
// TODO: Implement a more sophisticated adaptive logic
|
||||||
|
diff := completedDate.UTC().Sub(chore.NextDueDate.UTC())
|
||||||
|
baseDate = completedDate.UTC().Add(diff)
|
||||||
|
case "interval":
|
||||||
|
switch *frequencyMetadata.Unit {
|
||||||
|
case "hours":
|
||||||
|
baseDate = baseDate.Add(time.Duration(chore.Frequency) * time.Hour)
|
||||||
|
case "days":
|
||||||
|
baseDate = baseDate.AddDate(0, 0, chore.Frequency)
|
||||||
|
case "weeks":
|
||||||
|
baseDate = baseDate.AddDate(0, 0, chore.Frequency*7)
|
||||||
|
case "months":
|
||||||
|
baseDate = baseDate.AddDate(0, chore.Frequency, 0)
|
||||||
|
case "years":
|
||||||
|
baseDate = baseDate.AddDate(chore.Frequency, 0, 0)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid frequency unit: %s", *frequencyMetadata.Unit)
|
||||||
|
}
|
||||||
|
case "days_of_the_week":
|
||||||
|
if len(frequencyMetadata.Days) == 0 {
|
||||||
|
return nil, fmt.Errorf("days_of_the_week requires at least one day")
|
||||||
|
}
|
||||||
|
// Find the next valid day of the week
|
||||||
for i := 1; i <= 7; i++ {
|
for i := 1; i <= 7; i++ {
|
||||||
nextDueDate = baseDate.AddDate(0, 0, i)
|
nextDueDate := baseDate.AddDate(0, 0, i)
|
||||||
nextDay := strings.ToLower(nextDueDate.Weekday().String())
|
nextDay := strings.ToLower(nextDueDate.Weekday().String())
|
||||||
for _, day := range frequencyMetadata.Days {
|
for _, day := range frequencyMetadata.Days {
|
||||||
if strings.ToLower(*day) == nextDay {
|
if strings.ToLower(*day) == nextDay {
|
||||||
nextDate := nextDueDate.UTC()
|
return &nextDueDate, nil
|
||||||
return &nextDate, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if chore.FrequencyType == "day_of_the_month" {
|
return nil, fmt.Errorf("no matching day of the week found")
|
||||||
var frequencyMetadata chModel.FrequencyMetadata
|
case "day_of_the_month":
|
||||||
err := json.Unmarshal([]byte(*chore.FrequencyMetadata), &frequencyMetadata)
|
if len(frequencyMetadata.Months) == 0 {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("day_of_the_month requires at least one month")
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("error unmarshalling frequency metadata")
|
// Ensure the day of the month is valid
|
||||||
|
if chore.Frequency <= 0 || chore.Frequency > 31 {
|
||||||
|
return nil, fmt.Errorf("invalid day of the month: %d", chore.Frequency)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 1; i <= 12; i++ {
|
// Find the next valid day of the month, considering the year
|
||||||
nextDueDate = baseDate.AddDate(0, i, 0)
|
currentMonth := int(baseDate.Month())
|
||||||
// set the date to the first day of the month:
|
for i := 0; i < 12; i++ { // Start from 0 to check the current month first
|
||||||
nextDueDate = time.Date(nextDueDate.Year(), nextDueDate.Month(), chore.Frequency, nextDueDate.Hour(), nextDueDate.Minute(), 0, 0, nextDueDate.Location())
|
nextDueDate := baseDate.AddDate(0, i, 0)
|
||||||
nextMonth := strings.ToLower(nextDueDate.Month().String())
|
nextMonth := (currentMonth + i) % 12 // Use modulo to cycle through months
|
||||||
|
if nextMonth == 0 {
|
||||||
|
nextMonth = 12 // Adjust for December
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the target day exists in the month (e.g., Feb 30th is invalid)
|
||||||
|
lastDayOfMonth := time.Date(nextDueDate.Year(), time.Month(nextMonth+1), 0, 0, 0, 0, 0, time.UTC).Day()
|
||||||
|
targetDay := chore.Frequency
|
||||||
|
if targetDay > lastDayOfMonth {
|
||||||
|
targetDay = lastDayOfMonth
|
||||||
|
}
|
||||||
|
|
||||||
|
nextDueDate = time.Date(nextDueDate.Year(), time.Month(nextMonth), targetDay, nextDueDate.Hour(), nextDueDate.Minute(), 0, 0, time.UTC)
|
||||||
|
|
||||||
for _, month := range frequencyMetadata.Months {
|
for _, month := range frequencyMetadata.Months {
|
||||||
if *month == nextMonth {
|
if strings.ToLower(*month) == strings.ToLower(time.Month(nextMonth).String()) {
|
||||||
nextDate := nextDueDate.UTC()
|
return &nextDueDate, nil
|
||||||
return &nextDate, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if chore.FrequencyType == "no_repeat" {
|
return nil, fmt.Errorf("no matching month found")
|
||||||
return nil, nil
|
default:
|
||||||
} else if chore.FrequencyType == "trigger" {
|
return nil, fmt.Errorf("invalid frequency type: %s", chore.FrequencyType)
|
||||||
// if the chore is a trigger chore, then the next due date is nil
|
|
||||||
return nil, nil
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("invalid frequency type, cannot calculate next due date")
|
|
||||||
}
|
}
|
||||||
return &nextDueDate, nil
|
|
||||||
|
|
||||||
|
return &baseDate, nil
|
||||||
}
|
}
|
||||||
func scheduleAdaptiveNextDueDate(chore *chModel.Chore, completedDate time.Time, history []*chModel.ChoreHistory) (*time.Time, error) {
|
func scheduleAdaptiveNextDueDate(chore *chModel.Chore, completedDate time.Time, history []*chModel.ChoreHistory) (*time.Time, error) {
|
||||||
|
|
||||||
|
|
|
@ -1,312 +1,310 @@
|
||||||
package chore
|
package chore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
chModel "donetick.com/core/internal/chore/model"
|
chModel "donetick.com/core/internal/chore/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestScheduleNextDueDateBasic(t *testing.T) {
|
type scheduleTest struct {
|
||||||
choreTime := time.Now()
|
name string
|
||||||
freqencyMetadataBytes := `{"time":"2024-07-07T14:30:00-04:00"}`
|
chore chModel.Chore
|
||||||
intervalFreqencyMetadataBytes := `{"time":"2024-07-07T14:30:00-04:00", "unit": "days"}`
|
completedDate time.Time
|
||||||
|
want *time.Time
|
||||||
|
wantErr bool
|
||||||
|
wantErrMsg string
|
||||||
|
}
|
||||||
|
|
||||||
testTable := []struct {
|
func TestScheduleNextDueDateBasicTests(t *testing.T) {
|
||||||
Name string
|
// location, err := time.LoadLocation("America/New_York")
|
||||||
chore *chModel.Chore
|
location, err := time.LoadLocation("UTC")
|
||||||
completedAt time.Time
|
if err != nil {
|
||||||
expected time.Time
|
t.Fatalf("error loading location: %v", err)
|
||||||
}{
|
|
||||||
{
|
|
||||||
chore: &chModel.Chore{
|
|
||||||
FrequencyType: chModel.FrequencyTypeDaily,
|
|
||||||
NextDueDate: &choreTime,
|
|
||||||
FrequencyMetadata: &freqencyMetadataBytes,
|
|
||||||
},
|
|
||||||
completedAt: choreTime,
|
|
||||||
expected: choreTime.AddDate(0, 0, 1),
|
|
||||||
},
|
|
||||||
{ // Completed 1 day late
|
|
||||||
chore: &chModel.Chore{
|
|
||||||
FrequencyType: chModel.FrequencyTypeDaily,
|
|
||||||
NextDueDate: &choreTime,
|
|
||||||
FrequencyMetadata: &freqencyMetadataBytes,
|
|
||||||
},
|
|
||||||
completedAt: choreTime.AddDate(0, 0, 1),
|
|
||||||
expected: choreTime.AddDate(0, 0, 1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Rolling completed 1 day late",
|
|
||||||
chore: &chModel.Chore{
|
|
||||||
FrequencyType: chModel.FrequencyTypeDaily,
|
|
||||||
NextDueDate: &choreTime,
|
|
||||||
FrequencyMetadata: &freqencyMetadataBytes,
|
|
||||||
IsRolling: true,
|
|
||||||
},
|
|
||||||
completedAt: choreTime.AddDate(0, 0, 1),
|
|
||||||
expected: choreTime.AddDate(0, 0, 1+1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
chore: &chModel.Chore{
|
|
||||||
FrequencyType: chModel.FrequencyTypeWeekly,
|
|
||||||
NextDueDate: &choreTime,
|
|
||||||
FrequencyMetadata: &freqencyMetadataBytes,
|
|
||||||
},
|
|
||||||
completedAt: choreTime,
|
|
||||||
expected: choreTime.AddDate(0, 0, 7),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
chore: &chModel.Chore{
|
|
||||||
FrequencyType: chModel.FrequencyTypeMonthly,
|
|
||||||
NextDueDate: &choreTime,
|
|
||||||
FrequencyMetadata: &freqencyMetadataBytes,
|
|
||||||
},
|
|
||||||
completedAt: choreTime,
|
|
||||||
expected: choreTime.AddDate(0, 1, 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
chore: &chModel.Chore{
|
|
||||||
FrequencyType: chModel.FrequencyTypeYearly,
|
|
||||||
NextDueDate: &choreTime,
|
|
||||||
FrequencyMetadata: &freqencyMetadataBytes,
|
|
||||||
},
|
|
||||||
completedAt: choreTime,
|
|
||||||
expected: choreTime.AddDate(1, 0, 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "14 days interval Rolling Completed in time",
|
|
||||||
chore: &chModel.Chore{
|
|
||||||
FrequencyType: chModel.FrequencyTypeIntervel,
|
|
||||||
NextDueDate: &choreTime,
|
|
||||||
FrequencyMetadata: &intervalFreqencyMetadataBytes,
|
|
||||||
Frequency: 14,
|
|
||||||
IsRolling: true,
|
|
||||||
},
|
|
||||||
completedAt: choreTime,
|
|
||||||
expected: choreTime.AddDate(0, 0, 14),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "14 days interval Rolling Completed late",
|
|
||||||
chore: &chModel.Chore{
|
|
||||||
FrequencyType: chModel.FrequencyTypeIntervel,
|
|
||||||
NextDueDate: &choreTime,
|
|
||||||
FrequencyMetadata: &intervalFreqencyMetadataBytes,
|
|
||||||
Frequency: 14,
|
|
||||||
IsRolling: true,
|
|
||||||
},
|
|
||||||
completedAt: choreTime.AddDate(0, 0, 1),
|
|
||||||
expected: choreTime.AddDate(0, 0, 14+1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "14 days interval Completed in time",
|
|
||||||
chore: &chModel.Chore{
|
|
||||||
FrequencyType: chModel.FrequencyTypeIntervel,
|
|
||||||
NextDueDate: &choreTime,
|
|
||||||
FrequencyMetadata: &intervalFreqencyMetadataBytes,
|
|
||||||
Frequency: 14,
|
|
||||||
IsRolling: false,
|
|
||||||
},
|
|
||||||
completedAt: choreTime,
|
|
||||||
expected: truncateToDay(choreTime.AddDate(0, 0, 14).UTC()).Add(18 * time.Hour).Add(30 * time.Minute), // Note: Same Hour and Minute as Metadata time
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "14 days interval Completed late",
|
|
||||||
chore: &chModel.Chore{
|
|
||||||
FrequencyType: chModel.FrequencyTypeIntervel,
|
|
||||||
NextDueDate: &choreTime,
|
|
||||||
FrequencyMetadata: &intervalFreqencyMetadataBytes,
|
|
||||||
Frequency: 14,
|
|
||||||
IsRolling: false,
|
|
||||||
},
|
|
||||||
completedAt: choreTime.AddDate(0, 0, 1),
|
|
||||||
expected: truncateToDay(choreTime.AddDate(0, 0, 14).UTC()).Add(18 * time.Hour).Add(30 * time.Minute), // Note: Same Hour and Minute as Metadata time
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
}
|
}
|
||||||
for i, tt := range testTable {
|
|
||||||
t.Run(fmt.Sprintf("%s %s %d", tt.chore.FrequencyType, tt.Name, i), func(t *testing.T) {
|
|
||||||
|
|
||||||
actual, err := scheduleNextDueDate(tt.chore, tt.completedAt)
|
now := time.Date(2025, 1, 2, 0, 15, 0, 0, location)
|
||||||
if err != nil {
|
tests := []scheduleTest{
|
||||||
t.Errorf("Error: %v", err)
|
{
|
||||||
t.FailNow()
|
name: "Daily",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeDaily,
|
||||||
|
FrequencyMetadata: jsonPtr(`{"time":"2024-07-07T14:30:00-04:00"}`),
|
||||||
|
},
|
||||||
|
completedDate: now,
|
||||||
|
want: timePtr(now.AddDate(0, 0, 1)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Daily - (IsRolling)",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeDaily,
|
||||||
|
FrequencyMetadata: jsonPtr(`{"time":"2024-07-07T14:30:00-04:00"}`),
|
||||||
|
},
|
||||||
|
completedDate: now.AddDate(0, 1, 0),
|
||||||
|
want: timePtr(now.AddDate(0, 1, 1)),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Weekly",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeWeekly,
|
||||||
|
FrequencyMetadata: jsonPtr(`{"time":"2024-07-07T14:30:00-04:00"}`),
|
||||||
|
},
|
||||||
|
completedDate: now,
|
||||||
|
want: timePtr(now.AddDate(0, 0, 7)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Weekly - (IsRolling)",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeWeekly,
|
||||||
|
FrequencyMetadata: jsonPtr(`{"time":"2024-07-07T14:30:00-04:00"}`),
|
||||||
|
},
|
||||||
|
completedDate: now.AddDate(1, 0, 0),
|
||||||
|
want: timePtr(now.AddDate(1, 0, 7)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Monthly",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeMonthly,
|
||||||
|
FrequencyMetadata: jsonPtr(`{"time":"2024-07-07T14:30:00-04:00"}`),
|
||||||
|
},
|
||||||
|
completedDate: now,
|
||||||
|
want: timePtr(now.AddDate(0, 1, 0)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Monthly - (IsRolling)",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeMonthly,
|
||||||
|
FrequencyMetadata: jsonPtr(`{"time":"2024-07-07T14:30:00-04:00"}`),
|
||||||
|
},
|
||||||
|
completedDate: now.AddDate(0, 0, 2),
|
||||||
|
want: timePtr(now.AddDate(0, 1, 2)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Yearly",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeYearly,
|
||||||
|
FrequencyMetadata: jsonPtr(`{"time":"2024-07-07T14:30:00-04:00"}`),
|
||||||
|
},
|
||||||
|
completedDate: now,
|
||||||
|
want: timePtr(now.AddDate(1, 0, 0)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Yearly - (IsRolling)",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeYearly,
|
||||||
|
FrequencyMetadata: jsonPtr(`{"time":"2024-07-07T14:30:00-04:00"}`),
|
||||||
|
},
|
||||||
|
completedDate: now.AddDate(0, 0, 2),
|
||||||
|
want: timePtr(now.AddDate(1, 0, 2)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
executeTestTable(t, tests)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduleNextDueDateInterval(t *testing.T) {
|
||||||
|
// location, err := time.LoadLocation("America/New_York")
|
||||||
|
location, err := time.LoadLocation("UTC")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error loading location: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Date(2025, 1, 2, 0, 15, 0, 0, location)
|
||||||
|
tests := []scheduleTest{
|
||||||
|
{
|
||||||
|
name: "Interval - 2 Days",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeInterval,
|
||||||
|
Frequency: 2,
|
||||||
|
FrequencyMetadata: jsonPtr(`{"unit": "days","time":"2024-07-07T14:30:00-04:00"}`),
|
||||||
|
},
|
||||||
|
completedDate: now,
|
||||||
|
want: timePtr(truncateToDay(now.AddDate(0, 0, 2)).Add(14*time.Hour + 30*time.Minute)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Interval - 4 Weeks",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeInterval,
|
||||||
|
Frequency: 4,
|
||||||
|
FrequencyMetadata: jsonPtr(`{"unit": "weeks","time":"2024-07-07T14:30:00-04:00"}`),
|
||||||
|
},
|
||||||
|
completedDate: now,
|
||||||
|
want: timePtr(truncateToDay(now.AddDate(0, 0, 4*7)).Add(14*time.Hour + 30*time.Minute)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Interval - 3 Months",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeInterval,
|
||||||
|
Frequency: 3,
|
||||||
|
FrequencyMetadata: jsonPtr(`{"unit": "months","time":"2024-07-07T14:30:00-04:00"}`),
|
||||||
|
},
|
||||||
|
completedDate: now,
|
||||||
|
want: timePtr(truncateToDay(now.AddDate(0, 3, 0)).Add(14*time.Hour + 30*time.Minute)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Interval - 2 Years",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeInterval,
|
||||||
|
Frequency: 2,
|
||||||
|
FrequencyMetadata: jsonPtr(`{"unit": "years","time":"2024-07-07T14:30:00-04:00"}`),
|
||||||
|
},
|
||||||
|
completedDate: now,
|
||||||
|
want: timePtr(truncateToDay(now.AddDate(2, 0, 0)).Add(14*time.Hour + 30*time.Minute)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
executeTestTable(t, tests)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduleNextDueDateDayOfWeek(t *testing.T) {
|
||||||
|
// location, err := time.LoadLocation("America/New_York")
|
||||||
|
location, err := time.LoadLocation("UTC")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error loading location: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Date(2025, 1, 2, 0, 15, 0, 0, location)
|
||||||
|
tests := []scheduleTest{
|
||||||
|
{
|
||||||
|
name: "Days of the week - next Monday",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeDayOfTheWeek,
|
||||||
|
|
||||||
|
FrequencyMetadata: jsonPtr(`{"days": ["monday"], "time": "2025-01-20T18:00:00-05:00"}`),
|
||||||
|
},
|
||||||
|
completedDate: now,
|
||||||
|
want: func() *time.Time {
|
||||||
|
// Calculate next Monday at 18:00 EST
|
||||||
|
nextMonday := now.AddDate(0, 0, (int(time.Monday)-int(now.Weekday())+7)%7)
|
||||||
|
nextMonday = truncateToDay(nextMonday).Add(18*time.Hour + 0*time.Minute)
|
||||||
|
return &nextMonday
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Days of the week - next Monday(IsRolling)",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeDayOfTheWeek,
|
||||||
|
IsRolling: true,
|
||||||
|
FrequencyMetadata: jsonPtr(`{"days": ["monday"], "time": "2025-01-20T18:00:00-05:00"}`),
|
||||||
|
},
|
||||||
|
|
||||||
|
completedDate: now.AddDate(0, 1, 0),
|
||||||
|
want: func() *time.Time {
|
||||||
|
// Calculate next Thursday at 18:00 EST
|
||||||
|
completedDate := now.AddDate(0, 1, 0)
|
||||||
|
nextMonday := completedDate.AddDate(0, 0, (int(time.Monday)-int(completedDate.Weekday())+7)%7)
|
||||||
|
nextMonday = truncateToDay(nextMonday).Add(18*time.Hour + 0*time.Minute)
|
||||||
|
return &nextMonday
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
executeTestTable(t, tests)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduleNextDueDateDayOfMonth(t *testing.T) {
|
||||||
|
// location, err := time.LoadLocation("America/New_York")
|
||||||
|
location, err := time.LoadLocation("UTC")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error loading location: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Date(2025, 1, 2, 0, 15, 0, 0, location)
|
||||||
|
tests := []scheduleTest{
|
||||||
|
{
|
||||||
|
name: "Day of the month - 15th of January",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeDayOfTheMonth,
|
||||||
|
Frequency: 15,
|
||||||
|
FrequencyMetadata: jsonPtr(`{ "unit": "days", "time": "2025-01-20T18:00:00-05:00", "days": [], "months": [ "january" ] }`),
|
||||||
|
},
|
||||||
|
completedDate: now,
|
||||||
|
want: timePtr(time.Date(2025, 1, 15, 18, 0, 0, 0, location)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Day of the month - 15th of January(isRolling)",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: chModel.FrequencyTypeDayOfTheMonth,
|
||||||
|
Frequency: 15,
|
||||||
|
FrequencyMetadata: jsonPtr(`{ "unit": "days", "time": "2025-01-20T18:00:00-05:00", "days": [], "months": [ "january" ] }`),
|
||||||
|
},
|
||||||
|
completedDate: now.AddDate(1, 1, 0),
|
||||||
|
want: timePtr(time.Date(2027, 1, 15, 18, 0, 0, 0, location)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
executeTestTable(t, tests)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScheduleNextDueDateErrors(t *testing.T) {
|
||||||
|
// location, err := time.LoadLocation("America/New_York")
|
||||||
|
location, err := time.LoadLocation("UTC")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error loading location: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Date(2025, 1, 2, 0, 15, 0, 0, location)
|
||||||
|
tests := []scheduleTest{
|
||||||
|
{
|
||||||
|
name: "Invalid frequency Metadata",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
FrequencyType: "invalid",
|
||||||
|
FrequencyMetadata: jsonPtr(``),
|
||||||
|
},
|
||||||
|
completedDate: now,
|
||||||
|
wantErr: true,
|
||||||
|
wantErrMsg: "error unmarshalling frequency metadata: unexpected end of JSON input",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
executeTestTable(t, tests)
|
||||||
|
}
|
||||||
|
func TestScheduleNextDueDate(t *testing.T) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeTestTable(t *testing.T, tests []scheduleTest) {
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := scheduleNextDueDate(&tt.chore, tt.completedDate)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("testcase: %s", tt.name)
|
||||||
|
t.Errorf("scheduleNextDueDate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if actual == nil {
|
|
||||||
t.Errorf("Expected: %v, Error: Actual missing", tt.expected)
|
if tt.wantErr {
|
||||||
} else if actual.UTC().Format(time.RFC3339) != tt.expected.UTC().Format(time.RFC3339) {
|
if err.Error() != tt.wantErrMsg {
|
||||||
t.Errorf("Expected: %v, Actual: %v", tt.expected, actual)
|
t.Errorf("testcase: %s", tt.name)
|
||||||
|
t.Errorf("scheduleNextDueDate() error message = %v, wantErrMsg %v", err.Error(), tt.wantErrMsg)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !equalTime(got, tt.want) {
|
||||||
|
t.Errorf("testcase: %s", tt.name)
|
||||||
|
t.Errorf("scheduleNextDueDate() = %v, want %v", got, tt.want)
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func equalTime(t1, t2 *time.Time) bool {
|
||||||
|
if t1 == nil && t2 == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if t1 == nil || t2 == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return t1.Equal(*t2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func timePtr(t time.Time) *time.Time {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonPtr(s string) *string {
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
func truncateToDay(t time.Time) time.Time {
|
func truncateToDay(t time.Time) time.Time {
|
||||||
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||||
}
|
}
|
||||||
|
|
||||||
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.FrequencyTypeDayOfTheWeek,
|
|
||||||
NextDueDate: &nextSaturday,
|
|
||||||
},
|
|
||||||
frequencyMetadata: &chModel.FrequencyMetadata{
|
|
||||||
Time: timeOfChore,
|
|
||||||
Days: []*string{&Monday, &Wednesday},
|
|
||||||
},
|
|
||||||
|
|
||||||
expected: getExpectedTime(nextMonday, timeOfChore),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
chore: &chModel.Chore{
|
|
||||||
FrequencyType: chModel.FrequencyTypeDayOfTheWeek,
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue