diff --git a/internal/service/week_service_test.go b/internal/service/week_service_test.go index bed8d7a..b7e8e23 100644 --- a/internal/service/week_service_test.go +++ b/internal/service/week_service_test.go @@ -12,7 +12,7 @@ import ( "github.com/wotra/wotra/internal/store" ) -// weekTestAnchor is a fixed Tuesday (2026-W20) used as "now" across week service tests. +// weekTestAnchor is a fixed Wednesday (2026-W20) used as "now" across week service tests. var weekTestAnchor = time.Date(2026, 5, 13, 10, 0, 0, 0, time.UTC) func newFullServices(t *testing.T) (*service.EntryService, *service.DayService, *service.WeekService, *service.SettingsService, *clock.FixedClock) { @@ -166,31 +166,88 @@ func TestReopenWeek(t *testing.T) { } func TestCloseWeekMidWeek(t *testing.T) { - // weekTestAnchor = 2026-05-13 (Tuesday, 2026-W20). - // Mon 2026-05-11 and Tue 2026-05-13 are past workdays; Wed-Fri are future. + // weekTestAnchor = 2026-05-13 (Wednesday, 2026-W20). + // Mon–Wed are tracked with real intervals and closed. Thu is pre-marked as + // holiday, Fri as vacation. CloseWeek is called mid-week: the service skips + // future workdays (Thu, Fri), so only Mon+Tue+Wed intervals count. ctx := context.Background() - _, daySvc, weekSvc, _, clk := newFullServices(t) + entrySvc, daySvc, weekSvc, _, _ := newFullServices(t) - tz, _ := time.LoadLocation("UTC") - now := clk.Now().In(tz) - isoYear, isoWeek := now.ISOWeek() - weekKey := fmt.Sprintf("%d-W%02d", isoYear, isoWeek) + const weekKey = "2026-W20" + mon := time.Date(2026, 5, 11, 0, 0, 0, 0, time.UTC) + tue := time.Date(2026, 5, 12, 0, 0, 0, 0, time.UTC) + wed := time.Date(2026, 5, 13, 0, 0, 0, 0, time.UTC) + thu := time.Date(2026, 5, 14, 0, 0, 0, 0, time.UTC) + fri := time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC) - monday := now.AddDate(0, 0, -int(now.Weekday()-time.Monday)) - today := now.Format("2006-01-02") - for d := monday; d.Format("2006-01-02") <= today; d = d.AddDate(0, 0, 1) { - wd := d.Weekday() - if wd == time.Saturday || wd == time.Sunday { - continue - } - dk := d.Format("2006-01-02") - if _, err := daySvc.MarkDay(ctx, dk, domain.DayKindHoliday); err != nil { - t.Fatalf("MarkDay %s: %v", dk, err) - } + // at returns the unix-ms for a given day at hh:mm UTC. + at := func(base time.Time, h, m int) int64 { + return time.Date(base.Year(), base.Month(), base.Day(), h, m, 0, 0, time.UTC).UnixMilli() } - if _, err := weekSvc.CloseWeek(ctx, weekKey); err != nil { - t.Fatalf("CloseWeek mid-week: %v", err) + // Monday: 3 spans — 1.5h + 1h + 2h = 4.5h + for _, s := range [][2][2]int{{{9, 0}, {10, 30}}, {{11, 0}, {12, 0}}, {{13, 0}, {15, 0}}} { + if _, err := entrySvc.CreateInterval(ctx, service.CreateIntervalInput{ + StartTime: at(mon, s[0][0], s[0][1]), + EndTime: at(mon, s[1][0], s[1][1]), + }); err != nil { + t.Fatalf("CreateInterval mon: %v", err) + } + } + if _, err := daySvc.CloseDay(ctx, mon.Format("2006-01-02")); err != nil { + t.Fatalf("CloseDay mon: %v", err) + } + + // Tuesday: 1 span — 4h + if _, err := entrySvc.CreateInterval(ctx, service.CreateIntervalInput{ + StartTime: at(tue, 9, 0), + EndTime: at(tue, 13, 0), + }); err != nil { + t.Fatalf("CreateInterval tue: %v", err) + } + if _, err := daySvc.CloseDay(ctx, tue.Format("2006-01-02")); err != nil { + t.Fatalf("CloseDay tue: %v", err) + } + + // Wednesday: 2 spans — 2h + 2h = 4h + for _, s := range [][2][2]int{{{9, 0}, {11, 0}}, {{14, 0}, {16, 0}}} { + if _, err := entrySvc.CreateInterval(ctx, service.CreateIntervalInput{ + StartTime: at(wed, s[0][0], s[0][1]), + EndTime: at(wed, s[1][0], s[1][1]), + }); err != nil { + t.Fatalf("CreateInterval wed: %v", err) + } + } + if _, err := daySvc.CloseDay(ctx, wed.Format("2006-01-02")); err != nil { + t.Fatalf("CloseDay wed: %v", err) + } + + // Thursday and Friday are future — mark them ahead of time, so we can + // finish early. + if _, err := daySvc.MarkDay(ctx, thu.Format("2006-01-02"), domain.DayKindHoliday); err != nil { + t.Fatalf("MarkDay thu: %v", err) + } + if _, err := daySvc.MarkDay(ctx, fri.Format("2006-01-02"), domain.DayKindVacation); err != nil { + t.Fatalf("MarkDay fri: %v", err) + } + + cw, err := weekSvc.CloseWeek(ctx, weekKey) + if err != nil { + t.Fatalf("CloseWeek: %v", err) + } + + // Only Mon+Tue+Wed contribute: 4.5h + 4h + 4h + 8h + 8h = 28.5h + const wantWorked = 28*time.Hour + 30*time.Minute + if cw.WorkedMs != int64(wantWorked/time.Millisecond) { + t.Errorf("worked hours: want %s, got %s", wantWorked, time.Duration(cw.WorkedMs)*time.Millisecond) + } + const wantExpected = 40*time.Hour + if cw.ExpectedMs != int64(wantExpected/time.Millisecond) { + t.Errorf("expected_ms: want %s, got %s", wantExpected, time.Duration(cw.ExpectedMs)*time.Millisecond) + } + const wantDelta = wantWorked - wantExpected // −11.5h + if cw.DeltaMs != int64(wantDelta) { + t.Errorf("delta_ms: want %s, got %s", wantDelta, time.Duration(cw.DeltaMs)*time.Millisecond) } }