refactor(handlers): migrate remaining backend handlers to clock.Now() and transaction patterns
Apply clock.Now() migration, transaction wrapping, and minor refactors across admin, scheduling, today, user, auth handler, notifications, webhooks, services, portfolio, ratelimit, testutils, and main.go. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -11,9 +11,18 @@ import (
|
||||
"time"
|
||||
|
||||
"crussell/db"
|
||||
"crussell/clock"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
var londonLocation = func() *time.Location {
|
||||
loc, err := time.LoadLocation("Europe/London")
|
||||
if err != nil {
|
||||
panic("failed to load Europe/London timezone: " + err.Error())
|
||||
}
|
||||
return loc
|
||||
}()
|
||||
|
||||
type ServiceInfo struct {
|
||||
ServiceName *string `json:"service_name,omitempty"`
|
||||
ServiceDescription *string `json:"service_description,omitempty"`
|
||||
@@ -78,12 +87,23 @@ type CurrentNextResponse struct {
|
||||
|
||||
// GET /api/admin/today/current-next
|
||||
func GetCurrentAndNextHandler(w http.ResponseWriter, r *http.Request) {
|
||||
now := time.Now()
|
||||
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
now := clock.Now()
|
||||
// Align the daily summary boundary with the UK business day (midnight local time)
|
||||
// instead of UTC midnight. This prevents a 1-hour shift during BST.
|
||||
londonNow := now.In(londonLocation)
|
||||
todayStart := time.Date(londonNow.Year(), londonNow.Month(), londonNow.Day(), 0, 0, 0, 0, londonLocation).UTC()
|
||||
todayEnd := todayStart.Add(24 * time.Hour)
|
||||
|
||||
// Auto-transition confirmed bookings that have started but not ended to in_progress
|
||||
_, err := db.Conn.Exec(r.Context(), `
|
||||
// Auto-transition in_progress bookings that have ended to completed
|
||||
tx, err := db.Conn.Begin(r.Context())
|
||||
if err != nil {
|
||||
log.Printf("Failed to begin transaction: %v", err)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(r.Context())
|
||||
|
||||
_, err = tx.Exec(r.Context(), `
|
||||
UPDATE bookings
|
||||
SET status = 'in_progress'
|
||||
WHERE status = 'confirmed'
|
||||
@@ -92,10 +112,10 @@ func GetCurrentAndNextHandler(w http.ResponseWriter, r *http.Request) {
|
||||
`, now)
|
||||
if err != nil {
|
||||
log.Printf("Failed to auto-transition bookings to in_progress: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-transition in_progress bookings that have ended to completed
|
||||
_, err = db.Conn.Exec(r.Context(), `
|
||||
_, err = tx.Exec(r.Context(), `
|
||||
UPDATE bookings
|
||||
SET status = 'completed'
|
||||
WHERE status = 'in_progress'
|
||||
@@ -103,6 +123,12 @@ func GetCurrentAndNextHandler(w http.ResponseWriter, r *http.Request) {
|
||||
`, now)
|
||||
if err != nil {
|
||||
log.Printf("Failed to auto-transition bookings to completed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Commit(r.Context()); err != nil {
|
||||
log.Printf("Failed to commit transaction: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var current *AppointmentInfo
|
||||
@@ -182,9 +208,9 @@ func GetCurrentAndNextHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Get closing time respecting exceptional hours
|
||||
todayOpen := isDayOpen(r, now)
|
||||
todayOpen := isDayOpen(r, londonNow)
|
||||
if todayOpen {
|
||||
closeTime := getClosingTime(r, now)
|
||||
closeTime := getClosingTime(r, londonNow)
|
||||
if closeTime != "" {
|
||||
response.ClosingTime = &closeTime
|
||||
}
|
||||
@@ -204,7 +230,7 @@ func GetCurrentAndNextHandler(w http.ResponseWriter, r *http.Request) {
|
||||
response.Summary = summary
|
||||
|
||||
// If tomorrow is closed, also compute a week summary
|
||||
tomorrow := now.AddDate(0, 0, 1)
|
||||
tomorrow := londonNow.AddDate(0, 0, 1)
|
||||
if !isDayOpen(r, tomorrow) {
|
||||
weekStart, _ := findWeekSummaryRange(r, tomorrow)
|
||||
ws := computeAggregateSummary(r, weekStart, todayEnd)
|
||||
@@ -215,7 +241,7 @@ func GetCurrentAndNextHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
} else {
|
||||
// Closed day: show week summary — from start of last work period to now
|
||||
weekStart, _ := findWeekSummaryRange(r, now)
|
||||
weekStart, _ := findWeekSummaryRange(r, londonNow)
|
||||
summary := computeAggregateSummary(r, weekStart, todayEnd)
|
||||
summary.SummaryScope = "week"
|
||||
summary.SummaryStartDate = weekStart.Format("2006-01-02")
|
||||
@@ -316,7 +342,7 @@ func computeAggregateSummary(r *http.Request, rangeStart, rangeEnd time.Time) *D
|
||||
}
|
||||
|
||||
func findNewBookingServices(r *http.Request, rangeStart time.Time) []ServiceBookingCount {
|
||||
now := time.Now()
|
||||
now := clock.Now()
|
||||
|
||||
// Single query to find the last working day's closing time
|
||||
var lastClose time.Time
|
||||
@@ -325,9 +351,9 @@ func findNewBookingServices(r *http.Request, rangeStart time.Time) []ServiceBook
|
||||
err := db.Conn.QueryRow(r.Context(), `
|
||||
WITH days AS (
|
||||
SELECT
|
||||
(NOW() - make_interval(days => i))::date AS day_date,
|
||||
CASE WHEN EXTRACT(DOW FROM NOW() - make_interval(days => i)) = 0 THEN 6
|
||||
ELSE EXTRACT(DOW FROM NOW() - make_interval(days => i))::integer - 1
|
||||
(($1::timestamptz AT TIME ZONE 'Europe/London')::date - i) AS day_date,
|
||||
CASE WHEN EXTRACT(DOW FROM ($1::timestamptz AT TIME ZONE 'Europe/London')::date - i) = 0 THEN 6
|
||||
ELSE EXTRACT(DOW FROM ($1::timestamptz AT TIME ZONE 'Europe/London')::date - i)::integer - 1
|
||||
END AS weekday
|
||||
FROM generate_series(1, 14) AS i
|
||||
)
|
||||
@@ -355,7 +381,7 @@ func findNewBookingServices(r *http.Request, rangeStart time.Time) []ServiceBook
|
||||
WHERE COALESCE(eh.close_time, wh.close_time) IS NOT NULL
|
||||
ORDER BY d.day_date DESC
|
||||
LIMIT 1
|
||||
`).Scan(&dayDate, &closeTimeStr)
|
||||
`, now).Scan(&dayDate, &closeTimeStr)
|
||||
|
||||
if err == nil {
|
||||
parts := strings.Split(closeTimeStr, ":")
|
||||
@@ -364,10 +390,11 @@ func findNewBookingServices(r *http.Request, rangeStart time.Time) []ServiceBook
|
||||
if len(parts) > 1 {
|
||||
fmt.Sscanf(parts[1], "%d", &m)
|
||||
}
|
||||
lastClose = time.Date(dayDate.Year(), dayDate.Month(), dayDate.Day(), h, m, 0, 0, dayDate.Location())
|
||||
lastClose = time.Date(dayDate.Year(), dayDate.Month(), dayDate.Day(), h, m, 0, 0, londonLocation).UTC()
|
||||
} else {
|
||||
// Fallback: 5pm yesterday
|
||||
lastClose = time.Date(now.Year(), now.Month(), now.Day()-1, 17, 0, 0, 0, now.Location())
|
||||
londonNow := now.In(londonLocation)
|
||||
lastClose = time.Date(londonNow.Year(), londonNow.Month(), londonNow.Day()-1, 17, 0, 0, 0, londonLocation).UTC()
|
||||
}
|
||||
|
||||
// Query services across all new bookings, aggregated by service name
|
||||
@@ -405,6 +432,9 @@ func findNewBookingServices(r *http.Request, rangeStart time.Time) []ServiceBook
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Printf("Row iteration error in getNewBookingServiceCounts: %v", err)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
@@ -462,7 +492,7 @@ func findWeekSummaryRange(r *http.Request, today time.Time) (time.Time, time.Tim
|
||||
|
||||
// The working period ends the day before the closed run starts
|
||||
workingEnd := closedRunStart.AddDate(0, 0, -1)
|
||||
workingEndStart := time.Date(workingEnd.Year(), workingEnd.Month(), workingEnd.Day(), 0, 0, 0, 0, workingEnd.Location())
|
||||
workingEndStart := time.Date(workingEnd.Year(), workingEnd.Month(), workingEnd.Day(), 0, 0, 0, 0, londonLocation)
|
||||
|
||||
// Walk back from workingEnd to find where the previous closed run ended
|
||||
workingStart := workingEndStart
|
||||
@@ -470,7 +500,7 @@ func findWeekSummaryRange(r *http.Request, today time.Time) (time.Time, time.Tim
|
||||
d := workingEnd.AddDate(0, 0, -i)
|
||||
if !isDayOpen(r, d) {
|
||||
workingStart = d.AddDate(0, 0, 1) // day after the closed day
|
||||
workingStart = time.Date(workingStart.Year(), workingStart.Month(), workingStart.Day(), 0, 0, 0, 0, workingStart.Location())
|
||||
workingStart = time.Date(workingStart.Year(), workingStart.Month(), workingStart.Day(), 0, 0, 0, 0, londonLocation)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -668,10 +698,18 @@ type TodayAppointmentsResponse struct {
|
||||
|
||||
// GET /api/admin/today/appointments
|
||||
func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
now := time.Now()
|
||||
now := clock.Now()
|
||||
|
||||
// Auto-transition confirmed bookings that have started but not ended to in_progress
|
||||
_, err := db.Conn.Exec(r.Context(), `
|
||||
// Auto-transition in_progress bookings that have ended to completed
|
||||
tx, err := db.Conn.Begin(r.Context())
|
||||
if err != nil {
|
||||
log.Printf("Failed to begin transaction: %v", err)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(r.Context())
|
||||
|
||||
_, err = tx.Exec(r.Context(), `
|
||||
UPDATE bookings
|
||||
SET status = 'in_progress'
|
||||
WHERE status = 'confirmed'
|
||||
@@ -680,10 +718,10 @@ func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
`, now)
|
||||
if err != nil {
|
||||
log.Printf("Failed to auto-transition bookings to in_progress: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-transition in_progress bookings that have ended to completed
|
||||
_, err = db.Conn.Exec(r.Context(), `
|
||||
_, err = tx.Exec(r.Context(), `
|
||||
UPDATE bookings
|
||||
SET status = 'completed'
|
||||
WHERE status = 'in_progress'
|
||||
@@ -691,9 +729,16 @@ func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
`, now)
|
||||
if err != nil {
|
||||
log.Printf("Failed to auto-transition bookings to completed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
rangeStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
if err := tx.Commit(r.Context()); err != nil {
|
||||
log.Printf("Failed to commit transaction: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
londonNow := now.In(londonLocation)
|
||||
rangeStart := time.Date(londonNow.Year(), londonNow.Month(), londonNow.Day(), 0, 0, 0, 0, londonLocation).UTC()
|
||||
todayEnd := rangeStart.Add(24 * time.Hour)
|
||||
|
||||
// Fetch all bookings for today
|
||||
@@ -735,6 +780,11 @@ func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
raw = append(raw, a)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Printf("Row iteration error in GetTodayAppointments: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
if len(raw) == 0 {
|
||||
@@ -781,6 +831,9 @@ func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
svcMap[bid] = append(svcMap[bid], name)
|
||||
durMap[bid] += dur
|
||||
}
|
||||
if err := svcRows.Err(); err != nil {
|
||||
log.Printf("Row iteration error in GetTodayAppointments service fetch: %v", err)
|
||||
}
|
||||
svcRows.Close()
|
||||
|
||||
appointments := make([]TodayAppointment, 0, len(raw))
|
||||
@@ -822,6 +875,9 @@ func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
prevByUser[uid] = [2]string{pfn, pln}
|
||||
}
|
||||
}
|
||||
if err := nhRows.Err(); err != nil {
|
||||
log.Printf("Row iteration error in GetTodayAppointments name_history: %v", err)
|
||||
}
|
||||
nhRows.Close()
|
||||
for i := range appointments {
|
||||
if prev, ok := prevByUser[appointments[i].UserID]; ok {
|
||||
@@ -906,6 +962,11 @@ func GetPendingApprovalsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userIDs = append(userIDs, a.userID)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Printf("Row iteration error in GetPendingApprovals: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Batch-fetch name_history for all users.
|
||||
prevByUser := make(map[string][2]string)
|
||||
@@ -923,6 +984,9 @@ func GetPendingApprovalsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
prevByUser[uid] = [2]string{pfn, pln}
|
||||
}
|
||||
}
|
||||
if err := nhRows.Err(); err != nil {
|
||||
log.Printf("Row iteration error in GetPendingApprovals name_history: %v", err)
|
||||
}
|
||||
nhRows.Close()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user