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:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user