refactor: extract streak calculation to domain service

Move streak logic from application query to a dedicated domain service,
making it reusable and properly tested for all habit configurations.

- Support streaks for all habit types (boolean, counter, value) combined
  with positive/negative and optional target values
- Handle weekly habits with specific days (streak counts scheduled days)
- Today completed counts toward streak; not yet completed doesn't break it
- Calculate current and longest streak in a single forward pass
- Remove completion_rate from stats (not a useful metric for habits)
- Add comprehensive unit tests covering all 10 type combinations
This commit is contained in:
2026-03-07 22:43:48 +01:00
parent 77cfb709d8
commit 2b059ec334
7 changed files with 450 additions and 104 deletions
@@ -47,7 +47,7 @@ func TestHabitStatsFlow(t *testing.T) {
today := time.Now().UTC().Format("2006-01-02")
t.Run("Stats after marking habit once", func(t *testing.T) {
t.Run("Stats after marking habit today", func(t *testing.T) {
markReq := MarkHabitRequest{
ScheduledDate: today,
}
@@ -69,7 +69,7 @@ func TestHabitStatsFlow(t *testing.T) {
t.Errorf("Expected 1 total completion, got %d", stats.TotalCompletions)
}
if stats.CurrentStreak != 1 {
t.Errorf("Expected current streak of 1, got %d", stats.CurrentStreak)
t.Errorf("Expected current streak of 1 (today completed counts), got %d", stats.CurrentStreak)
}
if stats.LongestStreak != 1 {
t.Errorf("Expected longest streak of 1, got %d", stats.LongestStreak)