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:
@@ -46,9 +46,9 @@ func main() {
|
||||
settingsStore := store.NewSettingsStore(db)
|
||||
syncStore := store.NewSyncStore(db)
|
||||
|
||||
entrySvc := service.NewEntryService(entryStore, closedDayStore, settingsStore, tz)
|
||||
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, tz)
|
||||
settingsSvc := service.NewSettingsService(settingsStore)
|
||||
entrySvc := service.NewEntryService(entryStore, closedDayStore, settingsStore, syncStore, tz)
|
||||
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, syncStore, tz)
|
||||
settingsSvc := service.NewSettingsService(settingsStore, syncStore)
|
||||
weekSvc := service.NewWeekService(closedDayStore, closedWeekStore, entryStore, settingsStore, adjustmentStore, syncStore, db, tz)
|
||||
|
||||
// Background goroutine: auto-stop entries that cross midnight
|
||||
|
||||
@@ -22,6 +22,7 @@ type DayService struct {
|
||||
closedDays *store.ClosedDayStore
|
||||
closedWeeks *store.ClosedWeekStore
|
||||
settings *store.SettingsStore
|
||||
syncStore *store.SyncStore
|
||||
tz *time.Location
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@ func NewDayService(
|
||||
closedDays *store.ClosedDayStore,
|
||||
closedWeeks *store.ClosedWeekStore,
|
||||
settings *store.SettingsStore,
|
||||
syncStore *store.SyncStore,
|
||||
tz *time.Location,
|
||||
) *DayService {
|
||||
return &DayService{
|
||||
@@ -37,6 +39,7 @@ func NewDayService(
|
||||
closedDays: closedDays,
|
||||
closedWeeks: closedWeeks,
|
||||
settings: settings,
|
||||
syncStore: syncStore,
|
||||
tz: tz,
|
||||
}
|
||||
}
|
||||
@@ -74,7 +77,11 @@ func (s *DayService) recomputeWeek(ctx context.Context, dayKey string) error {
|
||||
cw.WorkedMs = totalWorkedMs
|
||||
cw.DeltaMs = totalWorkedMs - cw.ExpectedMs
|
||||
cw.UpdatedAt = time.Now().UnixMilli()
|
||||
return s.closedWeeks.Upsert(ctx, cw)
|
||||
if err := s.closedWeeks.Upsert(ctx, cw); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.syncStore.LogClosedWeek(ctx, cw)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseDay merges all completed entries for the given day key into a ClosedDay.
|
||||
@@ -136,6 +143,7 @@ func (s *DayService) CloseDay(ctx context.Context, dayKey string) (*domain.Close
|
||||
if err := s.closedDays.Upsert(ctx, cd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = s.syncStore.LogClosedDay(ctx, cd)
|
||||
if err := s.recomputeWeek(ctx, dayKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -175,6 +183,7 @@ func (s *DayService) MarkDay(ctx context.Context, dayKey string, kind domain.Day
|
||||
if err := s.closedDays.Upsert(ctx, cd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = s.syncStore.LogClosedDay(ctx, cd)
|
||||
if err := s.recomputeWeek(ctx, dayKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -196,6 +205,7 @@ func (s *DayService) ReopenDay(ctx context.Context, dayKey string) error {
|
||||
if err := s.closedDays.Delete(ctx, dayKey); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.syncStore.LogClosedDayDelete(ctx, dayKey)
|
||||
return s.recomputeWeek(ctx, dayKey)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,11 +22,12 @@ func newTestDayServices(t *testing.T) (*service.EntryService, *service.DayServic
|
||||
closedDayStore := store.NewClosedDayStore(db)
|
||||
closedWeekStore := store.NewClosedWeekStore(db)
|
||||
settingsStore := store.NewSettingsStore(db)
|
||||
syncStore := store.NewSyncStore(db)
|
||||
tz, _ := time.LoadLocation("UTC")
|
||||
|
||||
entrySvc := service.NewEntryService(entryStore, closedDayStore, settingsStore, tz)
|
||||
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, tz)
|
||||
settingsSvc := service.NewSettingsService(settingsStore)
|
||||
entrySvc := service.NewEntryService(entryStore, closedDayStore, settingsStore, syncStore, tz)
|
||||
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, syncStore, tz)
|
||||
settingsSvc := service.NewSettingsService(settingsStore, syncStore)
|
||||
return entrySvc, daySvc, settingsSvc
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func newTestServices(t *testing.T) *service.EntryService {
|
||||
settingsStore := store.NewSettingsStore(db)
|
||||
|
||||
tz, _ := time.LoadLocation("UTC")
|
||||
return service.NewEntryService(entryStore, closedDayStore, settingsStore, tz)
|
||||
return service.NewEntryService(entryStore, closedDayStore, settingsStore, store.NewSyncStore(db), tz)
|
||||
}
|
||||
|
||||
func TestStartStop(t *testing.T) {
|
||||
@@ -210,9 +210,10 @@ func TestUpdateRejectsClosedDay(t *testing.T) {
|
||||
closedDayStore := store.NewClosedDayStore(db)
|
||||
closedWeekStore := store.NewClosedWeekStore(db)
|
||||
settingsStore := store.NewSettingsStore(db)
|
||||
syncStore := store.NewSyncStore(db)
|
||||
tz, _ := time.LoadLocation("UTC")
|
||||
svc := service.NewEntryService(entryStore, closedDayStore, settingsStore, tz)
|
||||
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, tz)
|
||||
svc := service.NewEntryService(entryStore, closedDayStore, settingsStore, syncStore, tz)
|
||||
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, syncStore, tz)
|
||||
|
||||
entry, _ := svc.Start(ctx, "")
|
||||
svc.Stop(ctx)
|
||||
|
||||
@@ -22,11 +22,12 @@ var (
|
||||
|
||||
// SettingsService manages settings with effective-from history.
|
||||
type SettingsService struct {
|
||||
store *store.SettingsStore
|
||||
store *store.SettingsStore
|
||||
syncStore *store.SyncStore
|
||||
}
|
||||
|
||||
func NewSettingsService(s *store.SettingsStore) *SettingsService {
|
||||
return &SettingsService{store: s}
|
||||
func NewSettingsService(s *store.SettingsStore, syncStore *store.SyncStore) *SettingsService {
|
||||
return &SettingsService{store: s, syncStore: syncStore}
|
||||
}
|
||||
|
||||
// Current returns settings effective as of today.
|
||||
@@ -92,6 +93,7 @@ func (s *SettingsService) Upsert(ctx context.Context, input UpsertSettingsInput)
|
||||
if err := s.store.Insert(ctx, set); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = s.syncStore.LogSettings(ctx, set)
|
||||
return set, nil
|
||||
}
|
||||
|
||||
@@ -138,6 +140,7 @@ func (s *SettingsService) UpdateSettings(ctx context.Context, id string, input U
|
||||
if err := s.store.Update(ctx, set); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = s.syncStore.LogSettings(ctx, set)
|
||||
return set, nil
|
||||
}
|
||||
|
||||
@@ -157,5 +160,9 @@ func (s *SettingsService) DeleteSettings(ctx context.Context, id string) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
return s.store.Delete(ctx, id)
|
||||
if err := s.store.Delete(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.syncStore.LogSettingsDelete(ctx, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -238,6 +238,7 @@ func (s *WeekService) CloseWeek(ctx context.Context, weekKey string) (*domain.Cl
|
||||
if err := s.closedWeeks.Upsert(ctx, cw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = s.syncStore.LogClosedWeek(ctx, cw)
|
||||
return cw, nil
|
||||
}
|
||||
|
||||
@@ -254,7 +255,11 @@ func (s *WeekService) ReopenWeek(ctx context.Context, weekKey string) error {
|
||||
if existing == nil {
|
||||
return ErrWeekNotClosed
|
||||
}
|
||||
return s.closedWeeks.Delete(ctx, weekKey)
|
||||
if err := s.closedWeeks.Delete(ctx, weekKey); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.syncStore.LogClosedWeekDelete(ctx, weekKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListWeeks returns closed weeks within a range.
|
||||
|
||||
@@ -27,10 +27,10 @@ func newFullServices(t *testing.T) (*service.EntryService, *service.DayService,
|
||||
settingsStore := store.NewSettingsStore(db)
|
||||
tz, _ := time.LoadLocation("UTC")
|
||||
|
||||
entrySvc := service.NewEntryService(entryStore, closedDayStore, settingsStore, tz)
|
||||
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, tz)
|
||||
entrySvc := service.NewEntryService(entryStore, closedDayStore, settingsStore, syncStore, tz)
|
||||
daySvc := service.NewDayService(entryStore, closedDayStore, closedWeekStore, settingsStore, syncStore, tz)
|
||||
weekSvc := service.NewWeekService(closedDayStore, closedWeekStore, entryStore, settingsStore, adjustmentStore, syncStore, db, tz)
|
||||
settingsSvc := service.NewSettingsService(settingsStore)
|
||||
settingsSvc := service.NewSettingsService(settingsStore, syncStore)
|
||||
return entrySvc, daySvc, weekSvc, settingsSvc
|
||||
}
|
||||
|
||||
|
||||
@@ -150,6 +150,12 @@ func (s *SyncStore) LogClosedDay(ctx context.Context, d *domain.ClosedDay) error
|
||||
return s.log(ctx, "closed_days", d.DayKey, "upsert", string(payload))
|
||||
}
|
||||
|
||||
// LogClosedDayDelete appends a closed_day delete to the sync log.
|
||||
func (s *SyncStore) LogClosedDayDelete(ctx context.Context, dayKey string) error {
|
||||
payload := fmt.Sprintf(`{"day_key":%q}`, dayKey)
|
||||
return s.log(ctx, "closed_days", dayKey, "delete", payload)
|
||||
}
|
||||
|
||||
// LogClosedWeek appends a closed_week upsert to the sync log.
|
||||
func (s *SyncStore) LogClosedWeek(ctx context.Context, w *domain.ClosedWeek) error {
|
||||
payload, err := json.Marshal(w)
|
||||
@@ -159,6 +165,12 @@ func (s *SyncStore) LogClosedWeek(ctx context.Context, w *domain.ClosedWeek) err
|
||||
return s.log(ctx, "closed_weeks", w.WeekKey, "upsert", string(payload))
|
||||
}
|
||||
|
||||
// LogClosedWeekDelete appends a closed_week delete to the sync log.
|
||||
func (s *SyncStore) LogClosedWeekDelete(ctx context.Context, weekKey string) error {
|
||||
payload := fmt.Sprintf(`{"week_key":%q}`, weekKey)
|
||||
return s.log(ctx, "closed_weeks", weekKey, "delete", payload)
|
||||
}
|
||||
|
||||
// LogSettings appends a settings upsert to the sync log.
|
||||
func (s *SyncStore) LogSettings(ctx context.Context, set *domain.Settings) error {
|
||||
payload, err := json.Marshal(set)
|
||||
|
||||
Reference in New Issue
Block a user