diff --git a/internal/application/commands/create_habit.go b/internal/application/commands/create_habit.go index f743e4f..7927379 100644 --- a/internal/application/commands/create_habit.go +++ b/internal/application/commands/create_habit.go @@ -13,8 +13,8 @@ type CreateHabitCommand struct { UserID string Name string Description string - Type string - Frequency string + Type value_objects.HabitType + Frequency value_objects.Frequency SpecificDays []int SpecificDates []int CarryOver bool @@ -30,25 +30,23 @@ func NewCreateHabitHandler(habitRepo repositories.HabitRepository) *CreateHabitH } func (h *CreateHabitHandler) Handle(ctx context.Context, cmd CreateHabitCommand) (string, error) { - habitType := value_objects.HabitType(cmd.Type) - if !habitType.IsValid() { + if !cmd.Type.IsValid() { return "", errors.ErrInvalidInput } - frequency := value_objects.Frequency(cmd.Frequency) - if !frequency.IsValid() { + if !cmd.Frequency.IsValid() { return "", errors.ErrInvalidInput } - if frequency == value_objects.FrequencyWeekly && len(cmd.SpecificDays) == 0 { + if cmd.Frequency == value_objects.FrequencyWeekly && len(cmd.SpecificDays) == 0 { return "", errors.ErrInvalidInput } - if frequency == value_objects.FrequencyMonthly && len(cmd.SpecificDates) == 0 { + if cmd.Frequency == value_objects.FrequencyMonthly && len(cmd.SpecificDates) == 0 { return "", errors.ErrInvalidInput } - habit := entities.NewHabit(cmd.UserID, cmd.Name, habitType, frequency, cmd.CarryOver) + habit := entities.NewHabit(cmd.UserID, cmd.Name, cmd.Type, cmd.Frequency, cmd.CarryOver) habit.Description = cmd.Description habit.SpecificDays = cmd.SpecificDays habit.SpecificDates = cmd.SpecificDates diff --git a/internal/application/queries/get_habit_by_id.go b/internal/application/queries/get_habit_by_id.go index d18ec60..2a2fa9d 100644 --- a/internal/application/queries/get_habit_by_id.go +++ b/internal/application/queries/get_habit_by_id.go @@ -35,8 +35,8 @@ func (h *GetHabitByIDHandler) Handle(ctx context.Context, query GetHabitByIDQuer return &HabitDTO{ ID: habit.ID, Name: habit.Name, - Type: string(habit.Type), - Frequency: string(habit.Frequency), + Type: habit.Type, + Frequency: habit.Frequency, TargetValue: habit.TargetValue, CarryOver: habit.CarryOver, SpecificDays: habit.SpecificDays, diff --git a/internal/application/queries/get_habit_by_id_test.go b/internal/application/queries/get_habit_by_id_test.go index e3d8738..bcd98a4 100644 --- a/internal/application/queries/get_habit_by_id_test.go +++ b/internal/application/queries/get_habit_by_id_test.go @@ -53,7 +53,7 @@ func TestGetHabitByIDHandler_ReturnsHabitSuccessfully(t *testing.T) { t.Errorf("Expected name 'Drink Water', got %s", result.Name) } - if result.Type != string(value_objects.HabitTypeValue) { + if result.Type != value_objects.HabitTypeValue { t.Errorf("Expected type %s, got %s", value_objects.HabitTypeValue, result.Type) } diff --git a/internal/application/queries/get_todays_habits.go b/internal/application/queries/get_todays_habits.go index 274a235..8d9f98b 100644 --- a/internal/application/queries/get_todays_habits.go +++ b/internal/application/queries/get_todays_habits.go @@ -5,13 +5,14 @@ import ( "time" "apocapoc-api/internal/domain/repositories" + "apocapoc-api/internal/domain/value_objects" "apocapoc-api/internal/shared/utils" ) type TodaysHabitDTO struct { ID string Name string - Type string + Type value_objects.HabitType TargetValue *float64 ScheduledDate time.Time IsCarriedOver bool @@ -80,7 +81,7 @@ func (h *GetTodaysHabitsHandler) Handle( result = append(result, TodaysHabitDTO{ ID: habit.ID, Name: habit.Name, - Type: string(habit.Type), + Type: habit.Type, TargetValue: habit.TargetValue, ScheduledDate: query.Date, IsCarriedOver: !shouldAppear && habit.CarryOver, diff --git a/internal/application/queries/get_user_habits.go b/internal/application/queries/get_user_habits.go index de29c2e..fc95104 100644 --- a/internal/application/queries/get_user_habits.go +++ b/internal/application/queries/get_user_habits.go @@ -4,13 +4,14 @@ import ( "context" "apocapoc-api/internal/domain/repositories" + "apocapoc-api/internal/domain/value_objects" ) type HabitDTO struct { ID string Name string - Type string - Frequency string + Type value_objects.HabitType + Frequency value_objects.Frequency TargetValue *float64 CarryOver bool SpecificDays []int @@ -41,8 +42,8 @@ func (h *GetUserHabitsHandler) Handle(ctx context.Context, query GetUserHabitsQu result = append(result, HabitDTO{ ID: habit.ID, Name: habit.Name, - Type: string(habit.Type), - Frequency: string(habit.Frequency), + Type: habit.Type, + Frequency: habit.Frequency, TargetValue: habit.TargetValue, CarryOver: habit.CarryOver, SpecificDays: habit.SpecificDays, diff --git a/internal/application/queries/get_user_habits_test.go b/internal/application/queries/get_user_habits_test.go index 56826c5..39b5db8 100644 --- a/internal/application/queries/get_user_habits_test.go +++ b/internal/application/queries/get_user_habits_test.go @@ -92,11 +92,11 @@ func TestGetUserHabitsHandler_IncludesAllHabitFields(t *testing.T) { t.Errorf("Expected name 'Drink Water', got %s", result.Name) } - if result.Type != string(value_objects.HabitTypeValue) { + if result.Type != value_objects.HabitTypeValue { t.Errorf("Expected type %s, got %s", value_objects.HabitTypeValue, result.Type) } - if result.Frequency != string(value_objects.FrequencyDaily) { + if result.Frequency != value_objects.FrequencyDaily { t.Errorf("Expected frequency %s, got %s", value_objects.FrequencyDaily, result.Frequency) } diff --git a/internal/domain/value_objects/frequency.go b/internal/domain/value_objects/frequency.go index f5c17d9..1ac17d5 100644 --- a/internal/domain/value_objects/frequency.go +++ b/internal/domain/value_objects/frequency.go @@ -1,5 +1,10 @@ package value_objects +import ( + "encoding/json" + "fmt" +) + type Frequency string const ( @@ -15,3 +20,21 @@ func (f Frequency) IsValid() bool { } return false } + +func (f Frequency) MarshalJSON() ([]byte, error) { + return json.Marshal(string(f)) +} + +func (f *Frequency) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + *f = Frequency(s) + if !f.IsValid() { + return fmt.Errorf("invalid frequency: %s (must be DAILY, WEEKLY, or MONTHLY)", s) + } + + return nil +} diff --git a/internal/domain/value_objects/frequency_json_test.go b/internal/domain/value_objects/frequency_json_test.go new file mode 100644 index 0000000..41e00aa --- /dev/null +++ b/internal/domain/value_objects/frequency_json_test.go @@ -0,0 +1,90 @@ +package value_objects + +import ( + "encoding/json" + "testing" +) + +func TestFrequency_MarshalJSON(t *testing.T) { + tests := []struct { + name string + frequency Frequency + expected string + }{ + {"Daily", FrequencyDaily, `"DAILY"`}, + {"Weekly", FrequencyWeekly, `"WEEKLY"`}, + {"Monthly", FrequencyMonthly, `"MONTHLY"`}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.frequency) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + if string(data) != tt.expected { + t.Errorf("Expected %s, got %s", tt.expected, string(data)) + } + }) + } +} + +func TestFrequency_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + input string + expected Frequency + shouldErr bool + }{ + {"Valid Daily", `"DAILY"`, FrequencyDaily, false}, + {"Valid Weekly", `"WEEKLY"`, FrequencyWeekly, false}, + {"Valid Monthly", `"MONTHLY"`, FrequencyMonthly, false}, + {"Invalid frequency", `"YEARLY"`, "", true}, + {"Empty string", `""`, "", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var f Frequency + err := json.Unmarshal([]byte(tt.input), &f) + + if tt.shouldErr { + if err == nil { + t.Error("Expected error but got none") + } + return + } + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if f != tt.expected { + t.Errorf("Expected %s, got %s", tt.expected, f) + } + }) + } +} + +func TestFrequency_JSONRoundTrip(t *testing.T) { + type testStruct struct { + Frequency Frequency `json:"frequency"` + } + + original := testStruct{Frequency: FrequencyWeekly} + + data, err := json.Marshal(original) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + var decoded testStruct + err = json.Unmarshal(data, &decoded) + if err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if decoded.Frequency != original.Frequency { + t.Errorf("Round trip failed: expected %s, got %s", original.Frequency, decoded.Frequency) + } +} diff --git a/internal/domain/value_objects/habit_type.go b/internal/domain/value_objects/habit_type.go index baba5ff..525f9fb 100644 --- a/internal/domain/value_objects/habit_type.go +++ b/internal/domain/value_objects/habit_type.go @@ -1,5 +1,10 @@ package value_objects +import ( + "encoding/json" + "fmt" +) + type HabitType string const ( @@ -15,3 +20,21 @@ func (ht HabitType) IsValid() bool { } return false } + +func (ht HabitType) MarshalJSON() ([]byte, error) { + return json.Marshal(string(ht)) +} + +func (ht *HabitType) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + *ht = HabitType(s) + if !ht.IsValid() { + return fmt.Errorf("invalid habit type: %s (must be BOOLEAN, COUNTER, or VALUE)", s) + } + + return nil +} diff --git a/internal/domain/value_objects/habit_type_json_test.go b/internal/domain/value_objects/habit_type_json_test.go new file mode 100644 index 0000000..a0c5908 --- /dev/null +++ b/internal/domain/value_objects/habit_type_json_test.go @@ -0,0 +1,90 @@ +package value_objects + +import ( + "encoding/json" + "testing" +) + +func TestHabitType_MarshalJSON(t *testing.T) { + tests := []struct { + name string + habitType HabitType + expected string + }{ + {"Boolean", HabitTypeBoolean, `"BOOLEAN"`}, + {"Counter", HabitTypeCounter, `"COUNTER"`}, + {"Value", HabitTypeValue, `"VALUE"`}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.habitType) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + if string(data) != tt.expected { + t.Errorf("Expected %s, got %s", tt.expected, string(data)) + } + }) + } +} + +func TestHabitType_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + input string + expected HabitType + shouldErr bool + }{ + {"Valid Boolean", `"BOOLEAN"`, HabitTypeBoolean, false}, + {"Valid Counter", `"COUNTER"`, HabitTypeCounter, false}, + {"Valid Value", `"VALUE"`, HabitTypeValue, false}, + {"Invalid type", `"INVALID"`, "", true}, + {"Empty string", `""`, "", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ht HabitType + err := json.Unmarshal([]byte(tt.input), &ht) + + if tt.shouldErr { + if err == nil { + t.Error("Expected error but got none") + } + return + } + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if ht != tt.expected { + t.Errorf("Expected %s, got %s", tt.expected, ht) + } + }) + } +} + +func TestHabitType_JSONRoundTrip(t *testing.T) { + type testStruct struct { + Type HabitType `json:"type"` + } + + original := testStruct{Type: HabitTypeCounter} + + data, err := json.Marshal(original) + if err != nil { + t.Fatalf("Marshal failed: %v", err) + } + + var decoded testStruct + err = json.Unmarshal(data, &decoded) + if err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + + if decoded.Type != original.Type { + t.Errorf("Round trip failed: expected %s, got %s", original.Type, decoded.Type) + } +} diff --git a/internal/infrastructure/http/dto.go b/internal/infrastructure/http/dto.go index 8ae2974..eb6b397 100644 --- a/internal/infrastructure/http/dto.go +++ b/internal/infrastructure/http/dto.go @@ -1,16 +1,20 @@ package http -import "time" +import ( + "time" + + "apocapoc-api/internal/domain/value_objects" +) type CreateHabitRequest struct { - Name string `json:"name"` - Description string `json:"description"` - Type string `json:"type"` - Frequency string `json:"frequency"` - SpecificDays []int `json:"specific_days,omitempty"` - SpecificDates []int `json:"specific_dates,omitempty"` - CarryOver bool `json:"carry_over"` - TargetValue *float64 `json:"target_value,omitempty"` + Name string `json:"name"` + Description string `json:"description"` + Type value_objects.HabitType `json:"type"` + Frequency value_objects.Frequency `json:"frequency"` + SpecificDays []int `json:"specific_days,omitempty"` + SpecificDates []int `json:"specific_dates,omitempty"` + CarryOver bool `json:"carry_over"` + TargetValue *float64 `json:"target_value,omitempty"` } type UpdateHabitRequest struct { @@ -23,18 +27,18 @@ type UpdateHabitRequest struct { } type HabitResponse struct { - ID string `json:"id"` - UserID string `json:"user_id"` - Name string `json:"name"` - Description string `json:"description"` - Type string `json:"type"` - Frequency string `json:"frequency"` - SpecificDays []int `json:"specific_days,omitempty"` - SpecificDates []int `json:"specific_dates,omitempty"` - CarryOver bool `json:"carry_over"` - TargetValue *float64 `json:"target_value,omitempty"` - CreatedAt time.Time `json:"created_at"` - ArchivedAt *time.Time `json:"archived_at,omitempty"` + ID string `json:"id"` + UserID string `json:"user_id"` + Name string `json:"name"` + Description string `json:"description"` + Type value_objects.HabitType `json:"type"` + Frequency value_objects.Frequency `json:"frequency"` + SpecificDays []int `json:"specific_days,omitempty"` + SpecificDates []int `json:"specific_dates,omitempty"` + CarryOver bool `json:"carry_over"` + TargetValue *float64 `json:"target_value,omitempty"` + CreatedAt time.Time `json:"created_at"` + ArchivedAt *time.Time `json:"archived_at,omitempty"` } type MarkHabitRequest struct { @@ -43,22 +47,22 @@ type MarkHabitRequest struct { } type TodaysHabitResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - TargetValue *float64 `json:"target_value,omitempty"` - ScheduledDate time.Time `json:"scheduled_date"` - IsCarriedOver bool `json:"is_carried_over"` + ID string `json:"id"` + Name string `json:"name"` + Type value_objects.HabitType `json:"type"` + TargetValue *float64 `json:"target_value,omitempty"` + ScheduledDate time.Time `json:"scheduled_date"` + IsCarriedOver bool `json:"is_carried_over"` } type UserHabitResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Frequency string `json:"frequency"` - SpecificDays []int `json:"specific_days,omitempty"` - TargetValue *float64 `json:"target_value,omitempty"` - CarryOver bool `json:"carry_over"` + ID string `json:"id"` + Name string `json:"name"` + Type value_objects.HabitType `json:"type"` + Frequency value_objects.Frequency `json:"frequency"` + SpecificDays []int `json:"specific_days,omitempty"` + TargetValue *float64 `json:"target_value,omitempty"` + CarryOver bool `json:"carry_over"` } type HabitEntryResponse struct {