Complete CRUD operations for habits

Implement all missing endpoints for full habit management:
- GET /api/v1/habits - List all user habits
- GET /api/v1/habits/{id} - Get specific habit
- PUT /api/v1/habits/{id} - Update habit
- DELETE /api/v1/habits/{id} - Archive habit (soft delete)
- GET /api/v1/habits/{id}/entries - Get habit entry history
- DELETE /api/v1/habits/{id}/entries/{date} - Unmark habit (soft delete entry)

All endpoints include:
- TDD approach with comprehensive test coverage
- JWT authentication and ownership validation
- Proper error handling (404, 403, 400, 500)
- Clean architecture with separated commands/queries
This commit is contained in:
2025-11-26 14:50:11 +01:00
parent e87b7df979
commit 74cd2ec84d
16 changed files with 1474 additions and 7 deletions
@@ -0,0 +1,73 @@
package queries
import (
"context"
"time"
"apocapoc-api/internal/domain/repositories"
"apocapoc-api/internal/shared/errors"
)
type HabitEntryDTO struct {
ID string
HabitID string
ScheduledDate time.Time
CompletedAt time.Time
Value *float64
}
type GetHabitEntriesQuery struct {
HabitID string
UserID string
}
type GetHabitEntriesHandler struct {
habitRepo repositories.HabitRepository
entryRepo repositories.HabitEntryRepository
}
func NewGetHabitEntriesHandler(
habitRepo repositories.HabitRepository,
entryRepo repositories.HabitEntryRepository,
) *GetHabitEntriesHandler {
return &GetHabitEntriesHandler{
habitRepo: habitRepo,
entryRepo: entryRepo,
}
}
func (h *GetHabitEntriesHandler) Handle(ctx context.Context, query GetHabitEntriesQuery) ([]HabitEntryDTO, error) {
// Verify habit exists and user owns it
habit, err := h.habitRepo.FindByID(ctx, query.HabitID)
if err != nil {
return nil, err
}
if habit.UserID != query.UserID {
return nil, errors.ErrUnauthorized
}
// Get all entries for the habit
entries, err := h.entryRepo.FindByHabitID(ctx, query.HabitID)
if err != nil {
return nil, err
}
var result []HabitEntryDTO
for _, entry := range entries {
// Filter out deleted entries
if entry.DeletedAt != nil {
continue
}
result = append(result, HabitEntryDTO{
ID: entry.ID,
HabitID: entry.HabitID,
ScheduledDate: entry.ScheduledDate,
CompletedAt: entry.CompletedAt,
Value: entry.Value,
})
}
return result, nil
}