Stop auto-failing manual payments (TPago/bank/cash) after the pending TTL.

Exclude manual providers from booking cleanup, hold them via the 72h sweep instead, and let admins reopen failed payments to pending.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michilis
2026-07-20 03:53:51 +00:00
co-authored by Cursor
parent 0d47156071
commit 4772b85f3d
7 changed files with 267 additions and 53 deletions
+25 -12
View File
@@ -1,17 +1,23 @@
// Auto-hold stale pending-approval bookings.
// Auto-hold stale manual-payment bookings.
//
// A payment enters 'pending_approval' when a user clicks "I've paid" on a manual
// payment method (bank transfer / TPago) and is waiting for an admin to review it.
// If no admin acts within HOLD_THRESHOLD_HOURS, this job silently moves the payment
// (and its ticket) to 'on_hold', which drops it out of the capacity-counting statuses
// ('pending', 'confirmed', 'checked_in') and so releases the seat back to the event.
// The user receives no notification — they can recover via "I've paid" again, and an
// admin can reactivate or mark it paid directly, both re-checking capacity.
// This job releases the seat held by an abandoned manual-payment booking (bank
// transfer / TPago / cash) after HOLD_THRESHOLD_HOURS. It covers two states, both of
// which keep a seat reserved while awaiting a human:
// - 'pending_approval': the user clicked "I've paid" and is waiting for an admin.
// - 'pending' on a manual provider (see MANUAL_PAYMENT_PROVIDERS): the booking was
// never settled (these are exempt from the 30-min auto-fail in bookingCleanup.ts,
// so this is their only seat-release path).
// In either case the payment (and its ticket) is silently moved to 'on_hold', which
// drops it out of the capacity-counting statuses ('pending', 'confirmed', 'checked_in')
// and so releases the seat back to the event. The user receives no notification — they
// can recover via "I've paid" again, and an admin can reactivate or mark it paid
// directly, both re-checking capacity.
import { and, eq, lt, inArray } from 'drizzle-orm';
import { and, or, 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';
import { MANUAL_PAYMENT_PROVIDERS } from './manualProviders.js';
function getThresholdMs(): number {
const hours = parseInt(process.env.HOLD_THRESHOLD_HOURS || '72', 10);
@@ -19,7 +25,8 @@ function getThresholdMs(): number {
}
/**
* Move stale pending-approval payments (and their tickets) to 'on_hold'.
* Move stale awaiting-verification payments (and their tickets) to 'on_hold'.
* Covers 'pending_approval' payments and 'pending' payments on manual providers.
* Returns the number of payments put on hold.
*/
export async function sweepStaleApprovals(): Promise<number> {
@@ -33,7 +40,13 @@ export async function sweepStaleApprovals(): Promise<number> {
})
.from(payments)
.where(and(
eq((payments as any).status, 'pending_approval'),
or(
eq((payments as any).status, 'pending_approval'),
and(
eq((payments as any).status, 'pending'),
inArray((payments as any).provider, [...MANUAL_PAYMENT_PROVIDERS])
)
),
lt((payments as any).createdAt, cutoff)
))
);
@@ -59,7 +72,7 @@ export async function sweepStaleApprovals(): Promise<number> {
));
}
console.log(`[HoldSweep] Put ${stale.length} stale pending-approval payment(s) on hold.`);
console.log(`[HoldSweep] Put ${stale.length} stale awaiting-verification payment(s) on hold.`);
return stale.length;
}