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())
|
defer tx.Rollback(r.Context())
|
||||||
|
|
||||||
for _, h := range hours {
|
weekdays := make([]int, len(hours))
|
||||||
_, err := tx.Exec(r.Context(), `
|
startTimes := make([]string, len(hours))
|
||||||
UPDATE working_hours
|
endTimes := make([]string, len(hours))
|
||||||
SET start_time=$1,end_time=$2,is_open=$3
|
isOpenFlags := make([]bool, len(hours))
|
||||||
WHERE weekday=$4
|
for i, h := range hours {
|
||||||
`, h.StartTime, h.EndTime, h.IsOpen, h.Weekday)
|
weekdays[i] = h.Weekday
|
||||||
if err != nil {
|
startTimes[i] = h.StartTime
|
||||||
http.Error(w, "failed to update default hours", http.StatusInternalServerError)
|
endTimes[i] = h.EndTime
|
||||||
return
|
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 {
|
if err := tx.Commit(r.Context()); err != nil {
|
||||||
|
|||||||
@@ -218,15 +218,12 @@ func CreateExceptionalGroup(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Insert applications
|
// Insert applications
|
||||||
for _, weekStart := range parsedWeeks {
|
if _, err := tx.Exec(r.Context(), `
|
||||||
_, err := tx.Exec(r.Context(), `
|
INSERT INTO exceptional_group_applications (group_id, week_start)
|
||||||
INSERT INTO exceptional_group_applications (group_id, week_start)
|
SELECT $1, unnest($2::date[])
|
||||||
VALUES ($1, $2)
|
`, g.ID, parsedWeeks); err != nil {
|
||||||
`, g.ID, weekStart)
|
http.Error(w, "failed to insert applications", http.StatusInternalServerError)
|
||||||
if err != nil {
|
return
|
||||||
http.Error(w, "failed to insert application", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(r.Context()); err != nil {
|
if err := tx.Commit(r.Context()); err != nil {
|
||||||
@@ -328,15 +325,12 @@ func UpdateExceptionalApplications(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Insert new applications
|
// Insert new applications
|
||||||
for _, weekStart := range parsedWeeks {
|
if _, err := tx.Exec(r.Context(), `
|
||||||
_, err := tx.Exec(r.Context(), `
|
INSERT INTO exceptional_group_applications (group_id, week_start)
|
||||||
INSERT INTO exceptional_group_applications (group_id, week_start)
|
SELECT $1, unnest($2::date[])
|
||||||
VALUES ($1, $2)
|
`, req.GroupID, parsedWeeks); err != nil {
|
||||||
`, req.GroupID, weekStart)
|
http.Error(w, "failed to insert applications", http.StatusInternalServerError)
|
||||||
if err != nil {
|
return
|
||||||
http.Error(w, "failed to insert application", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(r.Context()); err != nil {
|
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
|
// Returns blockers for the given date range, expanded for recurring blockers
|
||||||
// Used by GetAvailableHours to subtract blocked time from available slots
|
// Used by GetAvailableHours to subtract blocked time from available slots
|
||||||
func GetTimeBlockersInRange(ctx context.Context, start, end time.Time) ([]TimeBlocker, error) {
|
func GetTimeBlockersInRange(ctx context.Context, start, end time.Time) ([]TimeBlocker, error) {
|
||||||
// Get one-off blockers in range
|
|
||||||
rows, err := db.Conn.Query(ctx, `
|
rows, err := db.Conn.Query(ctx, `
|
||||||
SELECT id, start_time, duration_minutes, description, cron_expression, created_at, created_by
|
SELECT id, start_time, duration_minutes, description, cron_expression, created_at, created_by
|
||||||
FROM time_blockers
|
FROM time_blockers
|
||||||
WHERE cron_expression IS NULL
|
WHERE (cron_expression IS NULL AND start_time >= $1 AND start_time <= $2)
|
||||||
AND start_time >= $1 AND start_time <= $2
|
OR (cron_expression IS NOT NULL)
|
||||||
ORDER BY start_time
|
ORDER BY start_time
|
||||||
`, start, end)
|
`, start, end)
|
||||||
if err != nil {
|
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 {
|
if err := rows.Scan(&b.ID, &b.StartTime, &b.DurationMinutes, &b.Description, &b.CronExpression, &b.CreatedAt, &b.CreatedBy); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
blockers = append(blockers, b)
|
if b.CronExpression != nil {
|
||||||
}
|
occurrences := expandCronOccurrences(b, start, end)
|
||||||
rows.Close()
|
blockers = append(blockers, occurrences...)
|
||||||
|
} else {
|
||||||
// Get ALL recurring blockers and expand them
|
blockers = append(blockers, b)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Expand recurring blocker to occurrences within range
|
if err := rows.Err(); err != nil {
|
||||||
occurrences := expandCronOccurrences(b, start, end)
|
return nil, err
|
||||||
blockers = append(blockers, occurrences...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return blockers, nil
|
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
|
// - Anonymized users: 7 years from payment creation AND 1 year since anonymization
|
||||||
//
|
//
|
||||||
// The function is idempotent — running it twice produces the same result.
|
// 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 {
|
func CleanupExpiredFinancialRecords(ctx context.Context) error {
|
||||||
tx, err := db.Conn.Begin(ctx)
|
tx, err := db.Conn.Begin(ctx)
|
||||||
if err != nil {
|
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 bookings b ON p.booking_id = b.id
|
||||||
LEFT JOIN users u ON b.user_id = u.id
|
LEFT JOIN users u ON b.user_id = u.id
|
||||||
WHERE p.created_at < NOW() - INTERVAL '7 years'
|
WHERE p.created_at < NOW() - INTERVAL '7 years'
|
||||||
AND (
|
` + retentionFilter + `
|
||||||
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'
|
|
||||||
)
|
|
||||||
GROUP BY DATE_TRUNC('month', p.created_at)::date
|
GROUP BY DATE_TRUNC('month', p.created_at)::date
|
||||||
ON CONFLICT (month) DO UPDATE SET
|
ON CONFLICT (month) DO UPDATE SET
|
||||||
total_payments = financial_aggregates.total_payments + EXCLUDED.total_payments,
|
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 bookings b ON p.booking_id = b.id
|
||||||
LEFT JOIN users u ON b.user_id = u.id
|
LEFT JOIN users u ON b.user_id = u.id
|
||||||
WHERE p.created_at < NOW() - INTERVAL '7 years'
|
WHERE p.created_at < NOW() - INTERVAL '7 years'
|
||||||
AND (
|
` + retentionFilter + `
|
||||||
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'
|
|
||||||
)
|
|
||||||
GROUP BY DATE_TRUNC('month', r.created_at)::date
|
GROUP BY DATE_TRUNC('month', r.created_at)::date
|
||||||
ON CONFLICT (month) DO UPDATE SET
|
ON CONFLICT (month) DO UPDATE SET
|
||||||
total_refunds = financial_aggregates.total_refunds + EXCLUDED.total_refunds
|
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
|
LEFT JOIN users u ON b.user_id = u.id
|
||||||
WHERE p.booking_id = b.id
|
WHERE p.booking_id = b.id
|
||||||
AND p.created_at < NOW() - INTERVAL '7 years'
|
AND p.created_at < NOW() - INTERVAL '7 years'
|
||||||
AND (
|
` + retentionFilter + `
|
||||||
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'
|
|
||||||
)
|
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to delete expired payments: %w", err)
|
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
|
LEFT JOIN users u ON b.user_id = u.id
|
||||||
WHERE r.payment_id = p.id
|
WHERE r.payment_id = p.id
|
||||||
AND p.created_at < NOW() - INTERVAL '7 years'
|
AND p.created_at < NOW() - INTERVAL '7 years'
|
||||||
AND (
|
` + retentionFilter + `
|
||||||
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'
|
|
||||||
)
|
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to delete expired refunds: %w", err)
|
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)
|
return fmt.Errorf("failed to batch zero account balances: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, id := range ids {
|
if _, err = tx.Exec(ctx, `
|
||||||
if _, err = tx.Exec(ctx, "SELECT anonymize_user($1)", id); err != nil {
|
SELECT anonymize_user(unnest($1::text[]))
|
||||||
return fmt.Errorf("failed to anonymize idle account %s: %w", id, err)
|
`, 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()
|
rowsNoBalance.Close()
|
||||||
|
|
||||||
for _, id := range accountsNoBalance {
|
if _, err = tx.Exec(ctx, `
|
||||||
if _, err = tx.Exec(ctx, "SELECT anonymize_user($1)", id); err != nil {
|
SELECT anonymize_user(unnest($1::text[]))
|
||||||
return fmt.Errorf("failed to anonymize idle account %s: %w", id, err)
|
`, accountsNoBalance); err != nil {
|
||||||
}
|
return fmt.Errorf("failed to anonymize idle accounts: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return tx.Commit(ctx)
|
return tx.Commit(ctx)
|
||||||
|
|||||||
Reference in New Issue
Block a user