fix(entries): reject create/update of intervals in the future

- Add ErrFutureDay sentinel error
- CreateInterval: rejects if startDayKey > todayKey (400)
- Update: rejects if new start_time moves entry to a future day (400)
- Handler maps ErrFutureDay → 400 Bad Request for both endpoints
- Add TestCreateIntervalRejectsFutureDay
- Add TestUpdateRejectsMoveToFutureDay
- UI already gates this via dayCapabilities (canAddInterval=false,
  canEditEntries=false for future days), but server now enforces it too
This commit is contained in:
2026-04-30 19:09:22 +02:00
parent 4c2b220482
commit bf0c728818
3 changed files with 70 additions and 2 deletions

View File

@@ -21,6 +21,7 @@ var (
ErrDayNotClosed = errors.New("day is not closed")
ErrRunningEntryOnDay = errors.New("a running entry exists for this day; stop it first")
ErrCrossesMidnight = errors.New("entry end_time must be on the same calendar day as start_time")
ErrFutureDay = errors.New("intervals cannot be created or edited in the future")
)
// EntryService handles business logic for time entries.
@@ -186,8 +187,13 @@ func (s *EntryService) Update(ctx context.Context, id string, input UpdateEntryI
}
if input.StartTime != nil {
newDayKey := s.dayKeyForMs(*input.StartTime)
// Reject if the new start_time moves the entry into the future.
if newDayKey > s.dayKeyForMs(s.nowMs()) {
return nil, ErrFutureDay
}
e.StartTime = *input.StartTime
e.DayKey = s.dayKeyForMs(e.StartTime)
e.DayKey = newDayKey
}
if input.EndTime != nil {
// Validate same-day
@@ -220,7 +226,7 @@ type CreateIntervalInput struct {
}
// CreateInterval adds a completed interval with explicit start and end times.
// Rules: same-day, end > start, day not closed, no currently running entry on that day (check running globally).
// Rules: same-day, end > start, day not closed, day not in the future.
func (s *EntryService) CreateInterval(ctx context.Context, input CreateIntervalInput) (*domain.Entry, error) {
if input.EndTime <= input.StartTime {
return nil, fmt.Errorf("end_time must be after start_time")
@@ -232,6 +238,12 @@ func (s *EntryService) CreateInterval(ctx context.Context, input CreateIntervalI
return nil, ErrCrossesMidnight
}
// Reject future intervals.
todayKey := s.dayKeyForMs(s.nowMs())
if startDayKey > todayKey {
return nil, ErrFutureDay
}
// Check day is not closed.
closed, err := s.closedDays.GetByDayKey(ctx, startDayKey)
if err != nil && !errors.Is(err, sql.ErrNoRows) {