diff --git a/compose.dev.yaml b/compose.dev.yaml index 559accc..da476b8 100644 --- a/compose.dev.yaml +++ b/compose.dev.yaml @@ -4,7 +4,7 @@ services: app: image: cosmtrek/air working_dir: /go/src/app - command: ["-c", ".air.toml"] + command: ["-c", ".air.toml"] depends_on: - postgres volumes: diff --git a/internal/models/tier_test.go b/internal/models/tier_test.go index 13cadba..362c467 100644 --- a/internal/models/tier_test.go +++ b/internal/models/tier_test.go @@ -3,16 +3,14 @@ package models import ( "testing" "time" - - "github.com/ubcesports/echo-base/internal/utils" ) func TestNewMembershipTier(t *testing.T) { tests := []struct { - name string - tierNumber int - wantName string - wantErr bool + name string + tierNumber int + wantName string + wantErr bool }{ {"Tier 0", 0, "No Membership", false}, {"Tier 1", 1, "Tier 1", false}, @@ -81,12 +79,7 @@ func TestTierDailyLimits(t *testing.T) { } func TestTierExpiryDates(t *testing.T) { - loc, err := utils.GetPacificLocation() - if err != nil { - t.Fatalf("GetPacificLocation() error = %v", err) - } - - now := time.Now().In(loc) + now := time.Now() currentYear := now.Year() expectedYear := currentYear if now.Month() >= time.May { @@ -135,19 +128,14 @@ func TestTierExpiryDates(t *testing.T) { } func TestTierIsExpired(t *testing.T) { - loc, err := utils.GetPacificLocation() - if err != nil { - t.Fatalf("GetPacificLocation() error = %v", err) - } - - yesterday := time.Now().In(loc).AddDate(0, 0, -1) - tomorrow := time.Now().In(loc).AddDate(0, 0, 1) - lastYear := time.Now().In(loc).AddDate(-1, 0, 0) + yesterday := time.Now().AddDate(0, 0, -1) + tomorrow := time.Now().AddDate(0, 0, 1) + lastYear := time.Now().AddDate(-1, 0, 0) tests := []struct { - name string - tierNumber int - expiryDate *time.Time + name string + tierNumber int + expiryDate *time.Time wantExpired bool }{ {"Tier 0 with nil expiry", 0, nil, false}, @@ -178,13 +166,8 @@ func TestTierIsExpired(t *testing.T) { } func TestTierExpiryBoundary(t *testing.T) { - loc, err := utils.GetPacificLocation() - if err != nil { - t.Fatalf("GetPacificLocation() error = %v", err) - } - - today := time.Now().In(loc) - todayStart := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, loc) + today := time.Now() + todayStart := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.UTC) tier, err := NewMembershipTier(1) if err != nil { diff --git a/internal/services/gamer_activity_service.go b/internal/services/gamer_activity_service.go index d51627b..acad2a7 100644 --- a/internal/services/gamer_activity_service.go +++ b/internal/services/gamer_activity_service.go @@ -3,11 +3,11 @@ package services import ( "context" "fmt" + "time" "github.com/ubcesports/echo-base/internal/errors" "github.com/ubcesports/echo-base/internal/interfaces/gamer" "github.com/ubcesports/echo-base/internal/models" - "github.com/ubcesports/echo-base/internal/utils" ) type gamerActivityService struct { @@ -82,16 +82,11 @@ func (s *gamerActivityService) StartActivity(ctx context.Context, req *models.Cr return nil, errors.NewForbiddenError(fmt.Sprintf("%s membership expired on %s. Please ask the user to purchase a new membership. If the member has already purchased a new membership for this year please verify via Showpass then create a new profile for them.", tier.GetName(), expiryDateStr)) } - startedAt, err := utils.NowInPacific() - if err != nil { - return nil, fmt.Errorf("failed to get current time: %w", err) - } - activity := &models.GamerActivity{ StudentNumber: req.StudentNumber, PCNumber: req.PCNumber, Game: req.Game, - StartedAt: startedAt, + StartedAt: time.Now(), } return s.activityRepo.Create(ctx, activity) @@ -106,12 +101,7 @@ func (s *gamerActivityService) EndActivity(ctx context.Context, studentNumber st return nil, errors.NewValidationError("exec_name", "is required") } - endedAt, err := utils.NowInPacific() - if err != nil { - return nil, fmt.Errorf("failed to get current time: %w", err) - } - - return s.activityRepo.UpdateEndTime(ctx, studentNumber, req.PCNumber, endedAt, req.ExecName) + return s.activityRepo.UpdateEndTime(ctx, studentNumber, req.PCNumber, time.Now(), req.ExecName) } func (s *gamerActivityService) GetActiveSessions(ctx context.Context) ([]models.GamerActivity, error) { diff --git a/internal/services/gamer_profile_service.go b/internal/services/gamer_profile_service.go index 31a8a28..abbd75d 100644 --- a/internal/services/gamer_profile_service.go +++ b/internal/services/gamer_profile_service.go @@ -4,11 +4,11 @@ import ( "context" "fmt" "regexp" + "time" "github.com/ubcesports/echo-base/internal/errors" "github.com/ubcesports/echo-base/internal/interfaces/gamer" "github.com/ubcesports/echo-base/internal/models" - "github.com/ubcesports/echo-base/internal/utils" ) var studentNumberRegex = regexp.MustCompile(`^\d{8}$`) @@ -52,11 +52,6 @@ func (s *gamerProfileService) CreateOrUpdateProfile(ctx context.Context, req *mo return nil, fmt.Errorf("failed to calculate expiry date: %w", err) } - createdAt, err := utils.NowInPacific() - if err != nil { - return nil, fmt.Errorf("failed to get current time: %w", err) - } - profile := &models.GamerProfile{ StudentNumber: req.StudentNumber, FirstName: req.FirstName, @@ -64,7 +59,7 @@ func (s *gamerProfileService) CreateOrUpdateProfile(ctx context.Context, req *mo MembershipTier: req.MembershipTier, Banned: req.Banned, Notes: req.Notes, - CreatedAt: createdAt, + CreatedAt: time.Now(), MembershipExpiryDate: expiryDate, } diff --git a/internal/utils/time.go b/internal/utils/time.go index 83359df..cb7ac56 100644 --- a/internal/utils/time.go +++ b/internal/utils/time.go @@ -1,66 +1,39 @@ package utils import ( - "fmt" "time" ) -func GetPacificLocation() (*time.Location, error) { - loc, err := time.LoadLocation("America/Los_Angeles") - if err != nil { - return nil, fmt.Errorf("failed to load Pacific timezone: %w", err) - } - return loc, nil -} - -func NowInPacific() (time.Time, error) { - loc, err := GetPacificLocation() - if err != nil { - return time.Time{}, err - } - return time.Now().In(loc), nil -} - -// GetNextMayFirst calculates the next May 1st in Pacific timezone +// GetNextMayFirst calculates the next May 1st // If current month >= May, returns May 1st of next year // Otherwise returns May 1st of current year -// This is because when memberships are created they expire on the +// This is because when memberships are created they expire on the // very next May first func GetNextMayFirst() (*time.Time, error) { - loc, err := GetPacificLocation() - if err != nil { - return nil, err - } - - now := time.Now().In(loc) + now := time.Now() year := now.Year() if now.Month() >= time.May { year++ } - expiry := time.Date(year, time.May, 1, 0, 0, 0, 0, loc) + expiry := time.Date(year, time.May, 1, 0, 0, 0, 0, time.UTC) return &expiry, nil } -// IsDateExpired compares expiryDate with today at day granularity in Pacific timezone +// IsDateExpired compares expiryDate with today // Returns true if today is after the expiry date, false if not expired or expiryDate is nil func IsDateExpired(expiryDate *time.Time) (bool, error) { if expiryDate == nil { return false, nil } - loc, err := GetPacificLocation() - if err != nil { - return false, err - } - // Get start of day for both dates for proper date-only comparison - now := time.Now().In(loc) - today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) + now := time.Now() + today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) - expiry := expiryDate.In(loc) - expiryStart := time.Date(expiry.Year(), expiry.Month(), expiry.Day(), 0, 0, 0, 0, loc) + expiry := expiryDate + expiryStart := time.Date(expiry.Year(), expiry.Month(), expiry.Day(), 0, 0, 0, 0, time.UTC) return today.After(expiryStart), nil } diff --git a/migrations/20260315125306-timestamptz.sql b/migrations/20260315125306-timestamptz.sql new file mode 100644 index 0000000..1ca80d6 --- /dev/null +++ b/migrations/20260315125306-timestamptz.sql @@ -0,0 +1,37 @@ +-- +migrate Up +ALTER TABLE gamer_profile + ALTER COLUMN created_at TYPE TIMESTAMPTZ + USING (created_at::timestamp AT TIME ZONE 'UTC'), + ALTER COLUMN membership_expiry_date TYPE TIMESTAMPTZ + USING (membership_expiry_date::timestamp AT TIME ZONE 'UTC'); + +ALTER TABLE gamer_activity + ALTER COLUMN started_at TYPE TIMESTAMPTZ + USING (started_at AT TIME ZONE 'UTC'), + ALTER COLUMN ended_at TYPE TIMESTAMPTZ + USING (ended_at AT TIME ZONE 'UTC'); + +ALTER TABLE application + ALTER COLUMN created_at TYPE TIMESTAMPTZ + USING (created_at AT TIME ZONE 'UTC'), + ALTER COLUMN last_used_at TYPE TIMESTAMPTZ + USING (last_used_at AT TIME ZONE 'UTC'); + +-- +migrate Down +ALTER TABLE application + ALTER COLUMN created_at TYPE TIMESTAMP + USING (created_at AT TIME ZONE 'UTC'), + ALTER COLUMN last_used_at TYPE TIMESTAMP + USING (last_used_at AT TIME ZONE 'UTC'); + +ALTER TABLE gamer_activity + ALTER COLUMN started_at TYPE TIMESTAMP + USING (started_at AT TIME ZONE 'UTC'), + ALTER COLUMN ended_at TYPE TIMESTAMP + USING (ended_at AT TIME ZONE 'UTC'); + +ALTER TABLE gamer_profile + ALTER COLUMN created_at TYPE DATE + USING ((created_at AT TIME ZONE 'UTC')::date), + ALTER COLUMN membership_expiry_date TYPE DATE + USING ((membership_expiry_date AT TIME ZONE 'UTC')::date);