fix: keep closed week snapshot in sync when days change
When a day is closed, re-closed, or reopened, DayService now recomputes worked_ms and delta_ms on the closed week containing that day (if the week is already closed). This prevents stale delta values after editing entries and re-closing a day. - DayService.recomputeWeek: sums worked_ms from all closed_days in the week, updates closed_weeks row preserving expected_ms - NewDayService now takes ClosedWeekStore - WeekKeyForDayKey exported helper (used by DayService) - TestWeekSnapshotUpdatesWhenDayReopened regression test
This commit is contained in:
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/wotra/wotra/internal/domain"
|
||||
"github.com/wotra/wotra/internal/store"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrWeekAlreadyClosed = errors.New("week is already closed")
|
||||
ErrWeekNotClosed = errors.New("week is not closed")
|
||||
@@ -19,26 +18,65 @@ var (
|
||||
|
||||
// DayService handles closing and marking days.
|
||||
type DayService struct {
|
||||
entries *store.EntryStore
|
||||
closedDays *store.ClosedDayStore
|
||||
settings *store.SettingsStore
|
||||
tz *time.Location
|
||||
entries *store.EntryStore
|
||||
closedDays *store.ClosedDayStore
|
||||
closedWeeks *store.ClosedWeekStore
|
||||
settings *store.SettingsStore
|
||||
tz *time.Location
|
||||
}
|
||||
|
||||
func NewDayService(
|
||||
entries *store.EntryStore,
|
||||
closedDays *store.ClosedDayStore,
|
||||
closedWeeks *store.ClosedWeekStore,
|
||||
settings *store.SettingsStore,
|
||||
tz *time.Location,
|
||||
) *DayService {
|
||||
return &DayService{
|
||||
entries: entries,
|
||||
closedDays: closedDays,
|
||||
settings: settings,
|
||||
tz: tz,
|
||||
entries: entries,
|
||||
closedDays: closedDays,
|
||||
closedWeeks: closedWeeks,
|
||||
settings: settings,
|
||||
tz: tz,
|
||||
}
|
||||
}
|
||||
|
||||
// recomputeWeek updates worked_ms/delta_ms on the closed week containing dayKey,
|
||||
// if that week is already closed. Called after any day close/reopen.
|
||||
func (s *DayService) recomputeWeek(ctx context.Context, dayKey string) error {
|
||||
weekKey, err := WeekKeyForDayKey(dayKey, s.tz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cw, err := s.closedWeeks.GetByWeekKey(ctx, weekKey)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil // week not closed yet — nothing to do
|
||||
}
|
||||
return err
|
||||
}
|
||||
// Recompute worked_ms from all closed days in the week.
|
||||
dayKeys, err := weekDayKeys(weekKey, s.tz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var totalWorkedMs int64
|
||||
for _, dk := range dayKeys {
|
||||
cd, err := s.closedDays.GetByDayKey(ctx, dk)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
totalWorkedMs += cd.WorkedMs
|
||||
}
|
||||
cw.WorkedMs = totalWorkedMs
|
||||
cw.DeltaMs = totalWorkedMs - cw.ExpectedMs
|
||||
cw.UpdatedAt = time.Now().UnixMilli()
|
||||
return s.closedWeeks.Upsert(ctx, cw)
|
||||
}
|
||||
|
||||
// CloseDay merges all completed entries for the given day key into a ClosedDay.
|
||||
// Returns ErrRunningEntryOnDay if a running entry exists.
|
||||
// Returns ErrDayAlreadyClosed if already closed.
|
||||
@@ -98,6 +136,9 @@ func (s *DayService) CloseDay(ctx context.Context, dayKey string) (*domain.Close
|
||||
if err := s.closedDays.Upsert(ctx, cd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.recomputeWeek(ctx, dayKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cd, nil
|
||||
}
|
||||
|
||||
@@ -134,6 +175,9 @@ func (s *DayService) MarkDay(ctx context.Context, dayKey string, kind domain.Day
|
||||
if err := s.closedDays.Upsert(ctx, cd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.recomputeWeek(ctx, dayKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cd, nil
|
||||
}
|
||||
|
||||
@@ -149,7 +193,10 @@ func (s *DayService) ReopenDay(ctx context.Context, dayKey string) error {
|
||||
if existing == nil {
|
||||
return ErrDayNotClosed
|
||||
}
|
||||
return s.closedDays.Delete(ctx, dayKey)
|
||||
if err := s.closedDays.Delete(ctx, dayKey); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.recomputeWeek(ctx, dayKey)
|
||||
}
|
||||
|
||||
// GetDay returns the closed day for a given day key, or nil if open.
|
||||
|
||||
Reference in New Issue
Block a user