Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compose.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
43 changes: 13 additions & 30 deletions internal/models/tier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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 {
Expand Down
16 changes: 3 additions & 13 deletions internal/services/gamer_activity_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
9 changes: 2 additions & 7 deletions internal/services/gamer_profile_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}$`)
Expand Down Expand Up @@ -52,19 +52,14 @@ 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,
LastName: req.LastName,
MembershipTier: req.MembershipTier,
Banned: req.Banned,
Notes: req.Notes,
CreatedAt: createdAt,
CreatedAt: time.Now(),
MembershipExpiryDate: expiryDate,
}

Expand Down
45 changes: 9 additions & 36 deletions internal/utils/time.go
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes more sense to compare the timestamps directly (i.e. including hours/minutes/seconds). If we want to be precise, we can ensure that the value we write is at 00:00 UTC-7 instead: But we are already writing 00:00 UTC+0, which is fine.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can include the migration, makes sense to me.

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
}
Expand Down
37 changes: 37 additions & 0 deletions migrations/20260315125306-timestamptz.sql
Original file line number Diff line number Diff line change
@@ -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);
Loading