package service import ( "context" "errors" "fmt" "time" "github.com/wotra/wotra/internal/domain" "github.com/wotra/wotra/internal/store" ) var ( ErrNoSettings = errors.New("no settings found") ErrInvalidWorkdaysMask = errors.New("workdays_mask must be between 1 and 127") ErrInvalidHours = errors.New("hours_per_week must be > 0") ) // SettingsService manages settings with effective-from history. type SettingsService struct { store *store.SettingsStore } func NewSettingsService(s *store.SettingsStore) *SettingsService { return &SettingsService{store: s} } // Current returns settings effective as of today. func (s *SettingsService) Current(ctx context.Context) (*domain.Settings, error) { today := time.Now().UTC().Format("2006-01-02") set, err := s.store.Current(ctx, today) if err != nil { return nil, ErrNoSettings } return set, nil } // AsOf returns settings effective on the given day key (YYYY-MM-DD). func (s *SettingsService) AsOf(ctx context.Context, dayKey string) (*domain.Settings, error) { set, err := s.store.Current(ctx, dayKey) if err != nil { return nil, ErrNoSettings } return set, nil } // History returns all settings rows, newest first. func (s *SettingsService) History(ctx context.Context) ([]*domain.Settings, error) { return s.store.History(ctx) } // UpsertInput is the payload for creating a new settings version. type UpsertSettingsInput struct { EffectiveFrom string HoursPerWeek float64 WorkdaysMask int Timezone string } // Upsert creates a new settings row (always inserts; effective_from allows retroactive changes). func (s *SettingsService) Upsert(ctx context.Context, input UpsertSettingsInput) (*domain.Settings, error) { if input.HoursPerWeek <= 0 { return nil, ErrInvalidHours } if input.WorkdaysMask < 1 || input.WorkdaysMask > 127 { return nil, ErrInvalidWorkdaysMask } if input.Timezone == "" { input.Timezone = "UTC" } if _, err := time.LoadLocation(input.Timezone); err != nil { return nil, fmt.Errorf("invalid timezone: %w", err) } if _, err := time.Parse("2006-01-02", input.EffectiveFrom); err != nil { return nil, fmt.Errorf("invalid effective_from: %w", err) } set := &domain.Settings{ EffectiveFrom: input.EffectiveFrom, HoursPerWeek: input.HoursPerWeek, WorkdaysMask: input.WorkdaysMask, Timezone: input.Timezone, CreatedAt: time.Now().UnixMilli(), } if err := s.store.Insert(ctx, set); err != nil { return nil, err } return set, nil }