M9.1: wire sync logging into all mutation paths
- Add LogClosedDayDelete and LogClosedWeekDelete to SyncStore - Inject syncStore into EntryService; log Start, Stop, StopByID, Update, CreateInterval, Delete, AutoStopStalledEntries - Inject syncStore into DayService; log CloseDay, MarkDay, ReopenDay, and the recomputeWeek closed-week upsert - Inject syncStore into SettingsService; log Upsert, UpdateSettings, DeleteSettings - Add LogClosedWeek/LogClosedWeekDelete calls in WeekService.CloseWeek and ReopenWeek - Update main.go and all service test helpers for new constructor signatures - All Go tests and 19 Vitest tests pass
This commit is contained in:
@@ -26,22 +26,25 @@ var (
|
||||
|
||||
// EntryService handles business logic for time entries.
|
||||
type EntryService struct {
|
||||
entries *store.EntryStore
|
||||
closedDays *store.ClosedDayStore
|
||||
settings *store.SettingsStore
|
||||
tz *time.Location
|
||||
entries *store.EntryStore
|
||||
closedDays *store.ClosedDayStore
|
||||
settings *store.SettingsStore
|
||||
syncStore *store.SyncStore
|
||||
tz *time.Location
|
||||
}
|
||||
|
||||
func NewEntryService(
|
||||
entries *store.EntryStore,
|
||||
closedDays *store.ClosedDayStore,
|
||||
settings *store.SettingsStore,
|
||||
syncStore *store.SyncStore,
|
||||
tz *time.Location,
|
||||
) *EntryService {
|
||||
return &EntryService{
|
||||
entries: entries,
|
||||
closedDays: closedDays,
|
||||
settings: settings,
|
||||
syncStore: syncStore,
|
||||
tz: tz,
|
||||
}
|
||||
}
|
||||
@@ -94,6 +97,7 @@ func (s *EntryService) Start(ctx context.Context, note string) (*domain.Entry, e
|
||||
if err := s.entries.Create(ctx, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = s.syncStore.LogEntry(ctx, e)
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -132,7 +136,6 @@ func (s *EntryService) stopEntry(ctx context.Context, e *domain.Entry, autoStopp
|
||||
startDayKey := s.dayKeyForMs(e.StartTime)
|
||||
endDayKey := s.dayKeyForMs(endMs)
|
||||
if endDayKey != startDayKey {
|
||||
// Cross-midnight: cap at end of start day
|
||||
endMs = s.midnightEndMs(e.StartTime)
|
||||
autoStopped = true
|
||||
}
|
||||
@@ -144,6 +147,7 @@ func (s *EntryService) stopEntry(ctx context.Context, e *domain.Entry, autoStopp
|
||||
if err := s.entries.Update(ctx, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = s.syncStore.LogEntry(ctx, e)
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -188,7 +192,6 @@ 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
|
||||
}
|
||||
@@ -196,7 +199,6 @@ func (s *EntryService) Update(ctx context.Context, id string, input UpdateEntryI
|
||||
e.DayKey = newDayKey
|
||||
}
|
||||
if input.EndTime != nil {
|
||||
// Validate same-day
|
||||
startDayKey := s.dayKeyForMs(e.StartTime)
|
||||
endDayKey := s.dayKeyForMs(*input.EndTime)
|
||||
if startDayKey != endDayKey {
|
||||
@@ -215,6 +217,7 @@ func (s *EntryService) Update(ctx context.Context, id string, input UpdateEntryI
|
||||
if err := s.entries.Update(ctx, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = s.syncStore.LogEntry(ctx, e)
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -226,7 +229,6 @@ type CreateIntervalInput struct {
|
||||
}
|
||||
|
||||
// CreateInterval adds a completed interval with explicit start and end times.
|
||||
// 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")
|
||||
@@ -238,13 +240,11 @@ 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) {
|
||||
return nil, err
|
||||
@@ -265,18 +265,34 @@ func (s *EntryService) CreateInterval(ctx context.Context, input CreateIntervalI
|
||||
if err := s.entries.Create(ctx, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = s.syncStore.LogEntry(ctx, e)
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Delete soft-deletes an entry.
|
||||
func (s *EntryService) Delete(ctx context.Context, id string) error {
|
||||
nowMs := s.nowMs()
|
||||
return s.entries.SoftDelete(ctx, id, nowMs)
|
||||
if err := s.entries.SoftDelete(ctx, id, nowMs); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.syncStore.LogEntryDelete(ctx, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoStopStalledEntries stops any running entries whose day_key is before today.
|
||||
// Called by the midnight background goroutine.
|
||||
func (s *EntryService) AutoStopStalledEntries(ctx context.Context) ([]string, error) {
|
||||
today := s.dayKeyForMs(s.nowMs())
|
||||
return s.entries.StopAllRunningBefore(ctx, today, s.nowMs(), s.nowMs())
|
||||
ids, err := s.entries.StopAllRunningBefore(ctx, today, s.nowMs(), s.nowMs())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Log each auto-stopped entry to sync.
|
||||
for _, id := range ids {
|
||||
e, err := s.entries.GetByID(ctx, id)
|
||||
if err == nil {
|
||||
_ = s.syncStore.LogEntry(ctx, e)
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user