Centralize seat capacity accounting and the payment provider registry.
Replace manualProviders.ts with a paymentProviders.ts registry (automatic vs manual settlement) and move all seat counting into capacity.ts as the single source of truth: only paid/checked-in tickets and pending_approval payments hold a seat, so abandoned checkouts never block sales. Admins can now knowingly approve a payment over capacity (allowOverCapacity), with the booking and admin UIs updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c9a600b6d6
commit
71c277045b
@@ -1,23 +1,24 @@
|
||||
// Auto-hold stale manual-payment bookings.
|
||||
// Auto-hold stale unsettled manual-payment bookings.
|
||||
//
|
||||
// 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.
|
||||
// This job moves abandoned manual-payment bookings (bank transfer / TPago / cash)
|
||||
// to 'on_hold' after HOLD_THRESHOLD_HOURS — bookings still in bare 'pending', i.e.
|
||||
// the customer never clicked "I've paid" and no admin settled them. These are exempt
|
||||
// from the 30-min auto-fail in bookingCleanup.ts, and under the capacity rule in
|
||||
// lib/capacity.ts they hold no seat, so this sweep is pure list hygiene: it keeps
|
||||
// dead checkouts out of the admin's pending queues.
|
||||
//
|
||||
// 'pending_approval' (customer claims they paid) is deliberately NOT swept: a
|
||||
// claimed payment keeps its seat until an admin approves or rejects it — the admin
|
||||
// UI surfaces aging claims instead of silently releasing them.
|
||||
//
|
||||
// The user receives no notification — they can recover via "I've paid", and an
|
||||
// admin can approve/reactivate directly; every recovery path re-checks capacity.
|
||||
|
||||
import { and, or, eq, lt, inArray } from 'drizzle-orm';
|
||||
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';
|
||||
import { MANUAL_PAYMENT_PROVIDERS } from './manualProviders.js';
|
||||
import { MANUAL_PAYMENT_PROVIDERS } from './paymentProviders.js';
|
||||
|
||||
function getThresholdMs(): number {
|
||||
const hours = parseInt(process.env.HOLD_THRESHOLD_HOURS || '72', 10);
|
||||
@@ -25,9 +26,9 @@ function getThresholdMs(): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Move stale unsettled manual payments (and their tickets) to 'on_hold'.
|
||||
* Covers only bare 'pending' payments on manual providers; 'pending_approval'
|
||||
* is never swept. Returns the number of payments put on hold.
|
||||
*/
|
||||
export async function sweepStaleApprovals(): Promise<number> {
|
||||
const cutoff = toDbDate(new Date(Date.now() - getThresholdMs()));
|
||||
@@ -40,13 +41,8 @@ export async function sweepStaleApprovals(): Promise<number> {
|
||||
})
|
||||
.from(payments)
|
||||
.where(and(
|
||||
or(
|
||||
eq((payments as any).status, 'pending_approval'),
|
||||
and(
|
||||
eq((payments as any).status, 'pending'),
|
||||
inArray((payments as any).provider, [...MANUAL_PAYMENT_PROVIDERS])
|
||||
)
|
||||
),
|
||||
eq((payments as any).status, 'pending'),
|
||||
inArray((payments as any).provider, [...MANUAL_PAYMENT_PROVIDERS]),
|
||||
lt((payments as any).createdAt, cutoff)
|
||||
))
|
||||
);
|
||||
@@ -72,7 +68,7 @@ export async function sweepStaleApprovals(): Promise<number> {
|
||||
));
|
||||
}
|
||||
|
||||
console.log(`[HoldSweep] Put ${stale.length} stale awaiting-verification payment(s) on hold.`);
|
||||
console.log(`[HoldSweep] Put ${stale.length} stale unsettled manual payment(s) on hold.`);
|
||||
return stale.length;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user