refactor(scheduling): batch queries and DRY retention filter

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-22 00:57:51 +01:00
co-authored by Sisyphus
parent 1485b55d92
commit bdee60d34f
3 changed files with 68 additions and 86 deletions
+26 -10
View File
@@ -92,16 +92,32 @@ func UpdateDefaultHours(w http.ResponseWriter, r *http.Request) {
}
defer tx.Rollback(r.Context())
for _, h := range hours {
_, err := tx.Exec(r.Context(), `
UPDATE working_hours
SET start_time=$1,end_time=$2,is_open=$3
WHERE weekday=$4
`, h.StartTime, h.EndTime, h.IsOpen, h.Weekday)
if err != nil {
http.Error(w, "failed to update default hours", http.StatusInternalServerError)
return
}
weekdays := make([]int, len(hours))
startTimes := make([]string, len(hours))
endTimes := make([]string, len(hours))
isOpenFlags := make([]bool, len(hours))
for i, h := range hours {
weekdays[i] = h.Weekday
startTimes[i] = h.StartTime
endTimes[i] = h.EndTime
isOpenFlags[i] = h.IsOpen
}
if _, err := tx.Exec(r.Context(), `
UPDATE working_hours AS wh
SET start_time = v.start_time,
end_time = v.end_time,
is_open = v.is_open
FROM (
SELECT unnest($1::smallint[]) AS weekday,
unnest($2::time[]) AS start_time,
unnest($3::time[]) AS end_time,
unnest($4::boolean[]) AS is_open
) v
WHERE wh.weekday = v.weekday
`, weekdays, startTimes, endTimes, isOpenFlags); err != nil {
http.Error(w, "failed to update default hours", http.StatusInternalServerError)
return
}
if err := tx.Commit(r.Context()); err != nil {
@@ -218,15 +218,12 @@ func CreateExceptionalGroup(w http.ResponseWriter, r *http.Request) {
}
// Insert applications
for _, weekStart := range parsedWeeks {
_, err := tx.Exec(r.Context(), `
INSERT INTO exceptional_group_applications (group_id, week_start)
VALUES ($1, $2)
`, g.ID, weekStart)
if err != nil {
http.Error(w, "failed to insert application", http.StatusInternalServerError)
return
}
if _, err := tx.Exec(r.Context(), `
INSERT INTO exceptional_group_applications (group_id, week_start)
SELECT $1, unnest($2::date[])
`, g.ID, parsedWeeks); err != nil {
http.Error(w, "failed to insert applications", http.StatusInternalServerError)
return
}
if err := tx.Commit(r.Context()); err != nil {
@@ -328,15 +325,12 @@ func UpdateExceptionalApplications(w http.ResponseWriter, r *http.Request) {
}
// Insert new applications
for _, weekStart := range parsedWeeks {
_, err := tx.Exec(r.Context(), `
INSERT INTO exceptional_group_applications (group_id, week_start)
VALUES ($1, $2)
`, req.GroupID, weekStart)
if err != nil {
http.Error(w, "failed to insert application", http.StatusInternalServerError)
return
}
if _, err := tx.Exec(r.Context(), `
INSERT INTO exceptional_group_applications (group_id, week_start)
SELECT $1, unnest($2::date[])
`, req.GroupID, parsedWeeks); err != nil {
http.Error(w, "failed to insert applications", http.StatusInternalServerError)
return
}
if err := tx.Commit(r.Context()); err != nil {
+30 -58
View File
@@ -197,12 +197,11 @@ func DeleteTimeBlocker(w http.ResponseWriter, r *http.Request) {
// Returns blockers for the given date range, expanded for recurring blockers
// Used by GetAvailableHours to subtract blocked time from available slots
func GetTimeBlockersInRange(ctx context.Context, start, end time.Time) ([]TimeBlocker, error) {
// Get one-off blockers in range
rows, err := db.Conn.Query(ctx, `
SELECT id, start_time, duration_minutes, description, cron_expression, created_at, created_by
FROM time_blockers
WHERE cron_expression IS NULL
AND start_time >= $1 AND start_time <= $2
WHERE (cron_expression IS NULL AND start_time >= $1 AND start_time <= $2)
OR (cron_expression IS NOT NULL)
ORDER BY start_time
`, start, end)
if err != nil {
@@ -216,30 +215,15 @@ func GetTimeBlockersInRange(ctx context.Context, start, end time.Time) ([]TimeBl
if err := rows.Scan(&b.ID, &b.StartTime, &b.DurationMinutes, &b.Description, &b.CronExpression, &b.CreatedAt, &b.CreatedBy); err != nil {
return nil, err
}
blockers = append(blockers, b)
}
rows.Close()
// Get ALL recurring blockers and expand them
recurringRows, err := db.Conn.Query(ctx, `
SELECT id, start_time, duration_minutes, description, cron_expression, created_at, created_by
FROM time_blockers
WHERE cron_expression IS NOT NULL
`)
if err != nil {
return nil, err
}
defer recurringRows.Close()
for recurringRows.Next() {
var b TimeBlocker
if err := recurringRows.Scan(&b.ID, &b.StartTime, &b.DurationMinutes, &b.Description, &b.CronExpression, &b.CreatedAt, &b.CreatedBy); err != nil {
return nil, err
if b.CronExpression != nil {
occurrences := expandCronOccurrences(b, start, end)
blockers = append(blockers, occurrences...)
} else {
blockers = append(blockers, b)
}
// Expand recurring blocker to occurrences within range
occurrences := expandCronOccurrences(b, start, end)
blockers = append(blockers, occurrences...)
}
if err := rows.Err(); err != nil {
return nil, err
}
return blockers, nil
@@ -459,6 +443,14 @@ func CleanupExpiredLoyaltyRedemptions(ctx context.Context) error {
// - Anonymized users: 7 years from payment creation AND 1 year since anonymization
//
// The function is idempotent — running it twice produces the same result.
const retentionFilter = `AND (
b.user_id IS NULL
OR u.id IS NULL
OR NOT (u.account_role = 'guest' AND (u.email LIKE 'anon-%@anon.invalid' OR u.email LIKE 'deleted+%@deleted.invalid'))
OR u.updated_at < NOW() - INTERVAL '1 year'
)`
func CleanupExpiredFinancialRecords(ctx context.Context) error {
tx, err := db.Conn.Begin(ctx)
if err != nil {
@@ -486,12 +478,7 @@ func CleanupExpiredFinancialRecords(ctx context.Context) error {
LEFT JOIN bookings b ON p.booking_id = b.id
LEFT JOIN users u ON b.user_id = u.id
WHERE p.created_at < NOW() - INTERVAL '7 years'
AND (
b.user_id IS NULL
OR u.id IS NULL
OR NOT (u.account_role = 'guest' AND (u.email LIKE 'anon-%@anon.invalid' OR u.email LIKE 'deleted+%@deleted.invalid'))
OR u.updated_at < NOW() - INTERVAL '1 year'
)
` + retentionFilter + `
GROUP BY DATE_TRUNC('month', p.created_at)::date
ON CONFLICT (month) DO UPDATE SET
total_payments = financial_aggregates.total_payments + EXCLUDED.total_payments,
@@ -521,12 +508,7 @@ func CleanupExpiredFinancialRecords(ctx context.Context) error {
LEFT JOIN bookings b ON p.booking_id = b.id
LEFT JOIN users u ON b.user_id = u.id
WHERE p.created_at < NOW() - INTERVAL '7 years'
AND (
b.user_id IS NULL
OR u.id IS NULL
OR NOT (u.account_role = 'guest' AND (u.email LIKE 'anon-%@anon.invalid' OR u.email LIKE 'deleted+%@deleted.invalid'))
OR u.updated_at < NOW() - INTERVAL '1 year'
)
` + retentionFilter + `
GROUP BY DATE_TRUNC('month', r.created_at)::date
ON CONFLICT (month) DO UPDATE SET
total_refunds = financial_aggregates.total_refunds + EXCLUDED.total_refunds
@@ -541,12 +523,7 @@ func CleanupExpiredFinancialRecords(ctx context.Context) error {
LEFT JOIN users u ON b.user_id = u.id
WHERE p.booking_id = b.id
AND p.created_at < NOW() - INTERVAL '7 years'
AND (
b.user_id IS NULL
OR u.id IS NULL
OR NOT (u.account_role = 'guest' AND (u.email LIKE 'anon-%@anon.invalid' OR u.email LIKE 'deleted+%@deleted.invalid'))
OR u.updated_at < NOW() - INTERVAL '1 year'
)
` + retentionFilter + `
`)
if err != nil {
return fmt.Errorf("failed to delete expired payments: %w", err)
@@ -559,12 +536,7 @@ func CleanupExpiredFinancialRecords(ctx context.Context) error {
LEFT JOIN users u ON b.user_id = u.id
WHERE r.payment_id = p.id
AND p.created_at < NOW() - INTERVAL '7 years'
AND (
b.user_id IS NULL
OR u.id IS NULL
OR NOT (u.account_role = 'guest' AND (u.email LIKE 'anon-%@anon.invalid' OR u.email LIKE 'deleted+%@deleted.invalid'))
OR u.updated_at < NOW() - INTERVAL '1 year'
)
` + retentionFilter + `
`)
if err != nil {
return fmt.Errorf("failed to delete expired refunds: %w", err)
@@ -867,10 +839,10 @@ func CleanupIdleAccounts(ctx context.Context) error {
return fmt.Errorf("failed to batch zero account balances: %w", err)
}
for _, id := range ids {
if _, err = tx.Exec(ctx, "SELECT anonymize_user($1)", id); err != nil {
return fmt.Errorf("failed to anonymize idle account %s: %w", id, err)
}
if _, err = tx.Exec(ctx, `
SELECT anonymize_user(unnest($1::text[]))
`, ids); err != nil {
return fmt.Errorf("failed to anonymize idle accounts: %w", err)
}
}
@@ -899,10 +871,10 @@ func CleanupIdleAccounts(ctx context.Context) error {
}
rowsNoBalance.Close()
for _, id := range accountsNoBalance {
if _, err = tx.Exec(ctx, "SELECT anonymize_user($1)", id); err != nil {
return fmt.Errorf("failed to anonymize idle account %s: %w", id, err)
}
if _, err = tx.Exec(ctx, `
SELECT anonymize_user(unnest($1::text[]))
`, accountsNoBalance); err != nil {
return fmt.Errorf("failed to anonymize idle accounts: %w", err)
}
return tx.Commit(ctx)