Add habit HTTP endpoints

- Create DTOs for HTTP requests and responses
- Implement habit handlers (create, get today's, mark)
- Register routes in router: POST /habits, GET /habits/today, POST /habits/{id}/mark
- Add missing repository methods (FindByID, FindByHabitID, FindPendingByHabitID, Delete)
- Wire up dependencies in main.go
- Tested with curl: create, list, mark habits work correctly
This commit is contained in:
2025-11-26 00:41:05 +01:00
parent 76893bd114
commit 3e8883d878
6 changed files with 341 additions and 2 deletions
+47
View File
@@ -0,0 +1,47 @@
package http
import "time"
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"`
}
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"`
}
type MarkHabitRequest struct {
ScheduledDate string `json:"scheduled_date"`
Value *float64 `json:"value,omitempty"`
}
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"`
}
type ErrorResponse struct {
Error string `json:"error"`
}
@@ -0,0 +1,143 @@
package http
import (
"encoding/json"
"net/http"
"time"
"habit-tracker-api/internal/application/commands"
"habit-tracker-api/internal/application/queries"
"habit-tracker-api/internal/shared/errors"
"github.com/go-chi/chi/v5"
)
type HabitHandlers struct {
createHandler *commands.CreateHabitHandler
getTodaysHandler *queries.GetTodaysHabitsHandler
markHandler *commands.MarkHabitHandler
}
func NewHabitHandlers(
createHandler *commands.CreateHabitHandler,
getTodaysHandler *queries.GetTodaysHabitsHandler,
markHandler *commands.MarkHabitHandler,
) *HabitHandlers {
return &HabitHandlers{
createHandler: createHandler,
getTodaysHandler: getTodaysHandler,
markHandler: markHandler,
}
}
func (h *HabitHandlers) CreateHabit(w http.ResponseWriter, r *http.Request) {
var req CreateHabitRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
userID := "user-123"
cmd := commands.CreateHabitCommand{
UserID: userID,
Name: req.Name,
Description: req.Description,
Type: req.Type,
Frequency: req.Frequency,
SpecificDays: req.SpecificDays,
SpecificDates: req.SpecificDates,
CarryOver: req.CarryOver,
TargetValue: req.TargetValue,
}
habitID, err := h.createHandler.Handle(r.Context(), cmd)
if err != nil {
if err == errors.ErrInvalidInput {
respondError(w, http.StatusBadRequest, err.Error())
return
}
respondError(w, http.StatusInternalServerError, "Failed to create habit")
return
}
respondJSON(w, http.StatusCreated, map[string]string{"id": habitID})
}
func (h *HabitHandlers) GetTodaysHabits(w http.ResponseWriter, r *http.Request) {
userID := "user-123"
timezone := "UTC"
query := queries.GetTodaysHabitsQuery{
UserID: userID,
Timezone: timezone,
Date: time.Now().UTC(),
}
habits, err := h.getTodaysHandler.Handle(r.Context(), query)
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to get habits")
return
}
response := make([]TodaysHabitResponse, len(habits))
for i, habit := range habits {
response[i] = TodaysHabitResponse{
ID: habit.ID,
Name: habit.Name,
Type: habit.Type,
TargetValue: habit.TargetValue,
ScheduledDate: habit.ScheduledDate,
IsCarriedOver: habit.IsCarriedOver,
}
}
respondJSON(w, http.StatusOK, response)
}
func (h *HabitHandlers) MarkHabit(w http.ResponseWriter, r *http.Request) {
habitID := chi.URLParam(r, "id")
var req MarkHabitRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
scheduledDate, err := time.Parse("2006-01-02", req.ScheduledDate)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid date format (use YYYY-MM-DD)")
return
}
cmd := commands.MarkHabitCommand{
HabitID: habitID,
ScheduledDate: scheduledDate,
Value: req.Value,
}
if err := h.markHandler.Handle(r.Context(), cmd); err != nil {
if err == errors.ErrAlreadyExists {
respondError(w, http.StatusConflict, "Habit already marked for this date")
return
}
if err == errors.ErrNotFound {
respondError(w, http.StatusNotFound, "Habit not found")
return
}
respondError(w, http.StatusInternalServerError, "Failed to mark habit")
return
}
respondJSON(w, http.StatusOK, map[string]string{"status": "marked"})
}
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, ErrorResponse{Error: message})
}
+7 -1
View File
@@ -8,7 +8,7 @@ import (
"github.com/go-chi/cors"
)
func NewRouter(corsOrigins string) *chi.Mux {
func NewRouter(corsOrigins string, habitHandlers *HabitHandlers) *chi.Mux {
r := chi.NewRouter()
r.Use(middleware.Logger)
@@ -25,5 +25,11 @@ func NewRouter(corsOrigins string) *chi.Mux {
w.Write([]byte(`{"status":"ok"}`))
})
r.Route("/api/v1/habits", func(r chi.Router) {
r.Post("/", habitHandlers.CreateHabit)
r.Get("/today", habitHandlers.GetTodaysHabits)
r.Post("/{id}/mark", habitHandlers.MarkHabit)
})
return r
}