Fix today's habits timezone calculation and include entry data
- Calculate today based on user's timezone instead of always using UTC - Include habit entry in response if it exists for the current day - Update GetTodaysHabitsHandler to fetch and return entry information - Add entry field to TodaysHabitDTO and TodaysHabitResponse - Update tests to reflect new behavior of including completed habits - Add test for habits with value entries
This commit is contained in:
+1
-1
@@ -108,7 +108,7 @@ func main() {
|
|||||||
unmarkHandler := commands.NewUnmarkHabitHandler(habitRepo, entryRepo)
|
unmarkHandler := commands.NewUnmarkHabitHandler(habitRepo, entryRepo)
|
||||||
|
|
||||||
authHandlers := httpInfra.NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry)
|
authHandlers := httpInfra.NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry)
|
||||||
habitHandlers := httpInfra.NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler)
|
habitHandlers := httpInfra.NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, userRepo)
|
||||||
statsHandlers := httpInfra.NewStatsHandlers(getHabitStatsHandler)
|
statsHandlers := httpInfra.NewStatsHandlers(getHabitStatsHandler)
|
||||||
healthHandlers := httpInfra.NewHealthHandlers(db.Conn())
|
healthHandlers := httpInfra.NewHealthHandlers(db.Conn())
|
||||||
userHandlers := httpInfra.NewUserHandlers(deleteUserHandler)
|
userHandlers := httpInfra.NewUserHandlers(deleteUserHandler)
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ import (
|
|||||||
"apocapoc-api/internal/shared/utils"
|
"apocapoc-api/internal/shared/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type TodaysHabitEntryDTO struct {
|
||||||
|
ID string
|
||||||
|
Value *float64
|
||||||
|
CompletedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type TodaysHabitDTO struct {
|
type TodaysHabitDTO struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
@@ -17,6 +23,7 @@ type TodaysHabitDTO struct {
|
|||||||
IsNegative bool
|
IsNegative bool
|
||||||
ScheduledDate time.Time
|
ScheduledDate time.Time
|
||||||
IsCarriedOver bool
|
IsCarriedOver bool
|
||||||
|
Entry *TodaysHabitEntryDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetTodaysHabitsQuery struct {
|
type GetTodaysHabitsQuery struct {
|
||||||
@@ -66,19 +73,19 @@ func (h *GetTodaysHabitsHandler) Handle(
|
|||||||
entries, _ := h.entryRepo.FindByHabitIDAndDateRange(
|
entries, _ := h.entryRepo.FindByHabitIDAndDateRange(
|
||||||
ctx,
|
ctx,
|
||||||
habit.ID,
|
habit.ID,
|
||||||
query.Date.AddDate(0, 0, -30),
|
query.Date,
|
||||||
query.Date,
|
query.Date,
|
||||||
)
|
)
|
||||||
|
|
||||||
isCompleted := false
|
var entryDTO *TodaysHabitEntryDTO
|
||||||
for _, entry := range entries {
|
if len(entries) > 0 && entries[0].ScheduledDate.Format("2006-01-02") == query.Date.Format("2006-01-02") {
|
||||||
if entry.ScheduledDate.Equal(query.Date) {
|
entryDTO = &TodaysHabitEntryDTO{
|
||||||
isCompleted = true
|
ID: entries[0].ID,
|
||||||
break
|
Value: entries[0].Value,
|
||||||
|
CompletedAt: entries[0].CompletedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isCompleted {
|
|
||||||
result = append(result, TodaysHabitDTO{
|
result = append(result, TodaysHabitDTO{
|
||||||
ID: habit.ID,
|
ID: habit.ID,
|
||||||
Name: habit.Name,
|
Name: habit.Name,
|
||||||
@@ -87,9 +94,9 @@ func (h *GetTodaysHabitsHandler) Handle(
|
|||||||
IsNegative: habit.IsNegative,
|
IsNegative: habit.IsNegative,
|
||||||
ScheduledDate: query.Date,
|
ScheduledDate: query.Date,
|
||||||
IsCarriedOver: !shouldAppear && habit.CarryOver,
|
IsCarriedOver: !shouldAppear && habit.CarryOver,
|
||||||
|
Entry: entryDTO,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ func TestGetTodaysHabitsHandler_DailyHabitNoEntries(t *testing.T) {
|
|||||||
if results[0].IsCarriedOver {
|
if results[0].IsCarriedOver {
|
||||||
t.Error("Expected IsCarriedOver to be false")
|
t.Error("Expected IsCarriedOver to be false")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if results[0].Entry != nil {
|
||||||
|
t.Error("Expected entry to be nil when no entry exists")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetTodaysHabitsHandler_DailyHabitAlreadyCompleted(t *testing.T) {
|
func TestGetTodaysHabitsHandler_DailyHabitAlreadyCompleted(t *testing.T) {
|
||||||
@@ -115,6 +119,7 @@ func TestGetTodaysHabitsHandler_DailyHabitAlreadyCompleted(t *testing.T) {
|
|||||||
|
|
||||||
targetDate := time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC)
|
targetDate := time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC)
|
||||||
entry := entities.NewHabitEntry("habit-1", targetDate, nil)
|
entry := entities.NewHabitEntry("habit-1", targetDate, nil)
|
||||||
|
entry.ID = "entry-1"
|
||||||
|
|
||||||
habitRepo := &mockHabitRepo{habits: []*entities.Habit{habit}}
|
habitRepo := &mockHabitRepo{habits: []*entities.Habit{habit}}
|
||||||
entryRepo := &mockEntryRepo{entries: []*entities.HabitEntry{entry}}
|
entryRepo := &mockEntryRepo{entries: []*entities.HabitEntry{entry}}
|
||||||
@@ -133,8 +138,61 @@ func TestGetTodaysHabitsHandler_DailyHabitAlreadyCompleted(t *testing.T) {
|
|||||||
t.Fatalf("Expected no error, got %v", err)
|
t.Fatalf("Expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(results) != 0 {
|
if len(results) != 1 {
|
||||||
t.Fatalf("Expected 0 habits (already completed), got %d", len(results))
|
t.Fatalf("Expected 1 habit (with entry), got %d", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
if results[0].Entry == nil {
|
||||||
|
t.Fatal("Expected entry to be present")
|
||||||
|
}
|
||||||
|
|
||||||
|
if results[0].Entry.ID != "entry-1" {
|
||||||
|
t.Errorf("Expected entry ID entry-1, got %s", results[0].Entry.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTodaysHabitsHandler_HabitWithValueEntry(t *testing.T) {
|
||||||
|
habit := entities.NewHabit("user-123", "Water", value_objects.HabitTypeValue, value_objects.FrequencyDaily, false, false)
|
||||||
|
habit.ID = "habit-1"
|
||||||
|
targetValue := 2000.0
|
||||||
|
habit.TargetValue = &targetValue
|
||||||
|
|
||||||
|
targetDate := time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC)
|
||||||
|
value := 1500.0
|
||||||
|
entry := entities.NewHabitEntry("habit-1", targetDate, &value)
|
||||||
|
entry.ID = "entry-1"
|
||||||
|
|
||||||
|
habitRepo := &mockHabitRepo{habits: []*entities.Habit{habit}}
|
||||||
|
entryRepo := &mockEntryRepo{entries: []*entities.HabitEntry{entry}}
|
||||||
|
|
||||||
|
handler := NewGetTodaysHabitsHandler(habitRepo, entryRepo)
|
||||||
|
|
||||||
|
query := GetTodaysHabitsQuery{
|
||||||
|
UserID: "user-123",
|
||||||
|
Timezone: "UTC",
|
||||||
|
Date: targetDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := handler.Handle(context.Background(), query)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Fatalf("Expected 1 habit, got %d", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
if results[0].Entry == nil {
|
||||||
|
t.Fatal("Expected entry to be present")
|
||||||
|
}
|
||||||
|
|
||||||
|
if results[0].Entry.Value == nil {
|
||||||
|
t.Fatal("Expected entry value to be present")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *results[0].Entry.Value != 1500.0 {
|
||||||
|
t.Errorf("Expected entry value 1500.0, got %f", *results[0].Entry.Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ type MarkHabitRequest struct {
|
|||||||
Value *float64 `json:"value,omitempty"`
|
Value *float64 `json:"value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TodaysHabitEntryResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Value *float64 `json:"value,omitempty"`
|
||||||
|
CompletedAt time.Time `json:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type TodaysHabitResponse struct {
|
type TodaysHabitResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -56,6 +62,7 @@ type TodaysHabitResponse struct {
|
|||||||
IsNegative bool `json:"is_negative"`
|
IsNegative bool `json:"is_negative"`
|
||||||
ScheduledDate time.Time `json:"scheduled_date"`
|
ScheduledDate time.Time `json:"scheduled_date"`
|
||||||
IsCarriedOver bool `json:"is_carried_over"`
|
IsCarriedOver bool `json:"is_carried_over"`
|
||||||
|
Entry *TodaysHabitEntryResponse `json:"entry,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserHabitResponse struct {
|
type UserHabitResponse struct {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"apocapoc-api/internal/application/commands"
|
"apocapoc-api/internal/application/commands"
|
||||||
"apocapoc-api/internal/application/queries"
|
"apocapoc-api/internal/application/queries"
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"apocapoc-api/internal/shared/errors"
|
"apocapoc-api/internal/shared/errors"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
@@ -24,6 +25,7 @@ type HabitHandlers struct {
|
|||||||
archiveHandler *commands.ArchiveHabitHandler
|
archiveHandler *commands.ArchiveHabitHandler
|
||||||
markHandler *commands.MarkHabitHandler
|
markHandler *commands.MarkHabitHandler
|
||||||
unmarkHandler *commands.UnmarkHabitHandler
|
unmarkHandler *commands.UnmarkHabitHandler
|
||||||
|
userRepo repositories.UserRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHabitHandlers(
|
func NewHabitHandlers(
|
||||||
@@ -36,6 +38,7 @@ func NewHabitHandlers(
|
|||||||
archiveHandler *commands.ArchiveHabitHandler,
|
archiveHandler *commands.ArchiveHabitHandler,
|
||||||
markHandler *commands.MarkHabitHandler,
|
markHandler *commands.MarkHabitHandler,
|
||||||
unmarkHandler *commands.UnmarkHabitHandler,
|
unmarkHandler *commands.UnmarkHabitHandler,
|
||||||
|
userRepo repositories.UserRepository,
|
||||||
) *HabitHandlers {
|
) *HabitHandlers {
|
||||||
return &HabitHandlers{
|
return &HabitHandlers{
|
||||||
createHandler: createHandler,
|
createHandler: createHandler,
|
||||||
@@ -47,6 +50,7 @@ func NewHabitHandlers(
|
|||||||
archiveHandler: archiveHandler,
|
archiveHandler: archiveHandler,
|
||||||
markHandler: markHandler,
|
markHandler: markHandler,
|
||||||
unmarkHandler: unmarkHandler,
|
unmarkHandler: unmarkHandler,
|
||||||
|
userRepo: userRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,7 +436,7 @@ func (h *HabitHandlers) GetHabitEntries(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
// GetTodaysHabits godoc
|
// GetTodaysHabits godoc
|
||||||
// @Summary Get today's habits
|
// @Summary Get today's habits
|
||||||
// @Description Get all habits scheduled for today for the authenticated user
|
// @Description Get all habits scheduled for today for the authenticated user. Includes the entry for today if it exists.
|
||||||
// @Tags habits
|
// @Tags habits
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
@@ -447,12 +451,24 @@ func (h *HabitHandlers) GetTodaysHabits(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
timezone := "UTC"
|
user, err := h.userRepo.FindByID(r.Context(), userID)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "Failed to get user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loc, err := time.LoadLocation(user.Timezone)
|
||||||
|
if err != nil {
|
||||||
|
loc = time.UTC
|
||||||
|
}
|
||||||
|
|
||||||
|
today := time.Now().In(loc)
|
||||||
|
todayDate := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
query := queries.GetTodaysHabitsQuery{
|
query := queries.GetTodaysHabitsQuery{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Timezone: timezone,
|
Timezone: user.Timezone,
|
||||||
Date: time.Now().UTC(),
|
Date: todayDate,
|
||||||
}
|
}
|
||||||
|
|
||||||
habits, err := h.getTodaysHandler.Handle(r.Context(), query)
|
habits, err := h.getTodaysHandler.Handle(r.Context(), query)
|
||||||
@@ -463,6 +479,15 @@ func (h *HabitHandlers) GetTodaysHabits(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
response := make([]TodaysHabitResponse, len(habits))
|
response := make([]TodaysHabitResponse, len(habits))
|
||||||
for i, habit := range habits {
|
for i, habit := range habits {
|
||||||
|
var entryResponse *TodaysHabitEntryResponse
|
||||||
|
if habit.Entry != nil {
|
||||||
|
entryResponse = &TodaysHabitEntryResponse{
|
||||||
|
ID: habit.Entry.ID,
|
||||||
|
Value: habit.Entry.Value,
|
||||||
|
CompletedAt: habit.Entry.CompletedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response[i] = TodaysHabitResponse{
|
response[i] = TodaysHabitResponse{
|
||||||
ID: habit.ID,
|
ID: habit.ID,
|
||||||
Name: habit.Name,
|
Name: habit.Name,
|
||||||
@@ -471,6 +496,7 @@ func (h *HabitHandlers) GetTodaysHabits(w http.ResponseWriter, r *http.Request)
|
|||||||
IsNegative: habit.IsNegative,
|
IsNegative: habit.IsNegative,
|
||||||
ScheduledDate: habit.ScheduledDate,
|
ScheduledDate: habit.ScheduledDate,
|
||||||
IsCarriedOver: habit.IsCarriedOver,
|
IsCarriedOver: habit.IsCarriedOver,
|
||||||
|
Entry: entryResponse,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func setupTestServer(t *testing.T) *TestServer {
|
|||||||
deleteUserHandler := commands.NewDeleteUserHandler(userRepo)
|
deleteUserHandler := commands.NewDeleteUserHandler(userRepo)
|
||||||
|
|
||||||
authHandlers := NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry)
|
authHandlers := NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry)
|
||||||
habitHandlers := NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler)
|
habitHandlers := NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, userRepo)
|
||||||
statsHandlers := NewStatsHandlers(getHabitStatsHandler)
|
statsHandlers := NewStatsHandlers(getHabitStatsHandler)
|
||||||
healthHandlers := NewHealthHandlers(db)
|
healthHandlers := NewHealthHandlers(db)
|
||||||
userHandlers := NewUserHandlers(deleteUserHandler)
|
userHandlers := NewUserHandlers(deleteUserHandler)
|
||||||
|
|||||||
Reference in New Issue
Block a user