Refactor monolithic modules and harden booking, email, and auth infrastructure.
Split oversized frontend API client, email service, and admin/booking pages into focused modules while preserving import surfaces, and add Redis-backed queues, stale booking cleanup, stronger auth, and scale deployment configs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
// Expire stale pending bookings.
|
||||
//
|
||||
// When a booking is started, its tickets are created with status 'pending' and
|
||||
// a 'pending' payment. Pending tickets count toward an event's capacity, so an
|
||||
// abandoned checkout would otherwise hold those seats forever. This job cancels
|
||||
// pending tickets whose payment is still 'pending' (i.e. never paid and not
|
||||
// awaiting admin approval) after a configurable TTL, freeing the seats.
|
||||
|
||||
import { and, eq, lt, inArray } from 'drizzle-orm';
|
||||
import { db, dbAll, tickets, payments } from '../db/index.js';
|
||||
import { getNow, toDbDate } from './utils.js';
|
||||
import { getLock } from './stores/lock.js';
|
||||
|
||||
function getTtlMs(): number {
|
||||
const minutes = parseInt(process.env.PENDING_BOOKING_TTL_MINUTES || '30', 10);
|
||||
return (Number.isFinite(minutes) && minutes > 0 ? minutes : 30) * 60 * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel stale pending bookings. Returns the number of tickets cancelled.
|
||||
*
|
||||
* A booking is considered stale when its payment is still 'pending' (not
|
||||
* 'pending_approval', which means an admin is reviewing a manual transfer) and
|
||||
* older than PENDING_BOOKING_TTL_MINUTES.
|
||||
*/
|
||||
export async function cleanupStalePendingBookings(): Promise<number> {
|
||||
const cutoff = toDbDate(new Date(Date.now() - getTtlMs()));
|
||||
|
||||
const stale = await dbAll<{ ticketId: string | null; paymentId: string }>(
|
||||
(db as any)
|
||||
.select({
|
||||
ticketId: (payments as any).ticketId,
|
||||
paymentId: (payments as any).id,
|
||||
})
|
||||
.from(payments)
|
||||
.where(and(
|
||||
eq((payments as any).status, 'pending'),
|
||||
lt((payments as any).createdAt, cutoff)
|
||||
))
|
||||
);
|
||||
|
||||
if (stale.length === 0) return 0;
|
||||
|
||||
const ticketIds = stale.map((s) => s.ticketId).filter((id): id is string => !!id);
|
||||
const paymentIds = stale.map((s) => s.paymentId);
|
||||
const now = getNow();
|
||||
|
||||
let cancelledTickets = 0;
|
||||
if (ticketIds.length > 0) {
|
||||
const result: any = await (db as any)
|
||||
.update(tickets)
|
||||
.set({ status: 'cancelled' })
|
||||
.where(and(
|
||||
inArray((tickets as any).id, ticketIds),
|
||||
eq((tickets as any).status, 'pending')
|
||||
));
|
||||
cancelledTickets = result?.changes ?? result?.rowCount ?? ticketIds.length;
|
||||
}
|
||||
|
||||
await (db as any)
|
||||
.update(payments)
|
||||
.set({ status: 'failed', updatedAt: now })
|
||||
.where(inArray((payments as any).id, paymentIds));
|
||||
|
||||
console.log(
|
||||
`[BookingCleanup] Expired ${stale.length} stale pending payment(s); ` +
|
||||
`cancelled ${cancelledTickets} ticket(s).`
|
||||
);
|
||||
return cancelledTickets;
|
||||
}
|
||||
|
||||
let cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
/**
|
||||
* Start a periodic cleanup of stale pending bookings. Each run is guarded by a
|
||||
* distributed lock so that, across multiple replicas, only one instance does
|
||||
* the work per interval.
|
||||
*/
|
||||
export function startBookingCleanup(): void {
|
||||
const intervalMs = parseInt(process.env.PENDING_BOOKING_CLEANUP_INTERVAL_MS || '300000', 10); // 5 min
|
||||
|
||||
const run = () => {
|
||||
getLock()
|
||||
.withLock('cleanup-pending-bookings', Math.min(intervalMs, 60_000), () =>
|
||||
cleanupStalePendingBookings()
|
||||
)
|
||||
.catch((err) =>
|
||||
console.error('[BookingCleanup] Run failed:', err?.message || err)
|
||||
);
|
||||
};
|
||||
|
||||
// Run shortly after startup, then on the interval.
|
||||
setTimeout(run, 30_000).unref?.();
|
||||
cleanupTimer = setInterval(run, intervalMs);
|
||||
cleanupTimer.unref?.();
|
||||
console.log(`[BookingCleanup] Scheduled every ${Math.round(intervalMs / 1000)}s`);
|
||||
}
|
||||
|
||||
export function stopBookingCleanup(): void {
|
||||
if (cleanupTimer) {
|
||||
clearInterval(cleanupTimer);
|
||||
cleanupTimer = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user