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
@@ -3,6 +3,7 @@ import { db, dbGet, dbAll, users, events, tickets, payments, contacts, emailSubs
|
||||
import { eq, and, ne, gte, sql, desc, inArray } from 'drizzle-orm';
|
||||
import { requireAuth } from '../lib/auth.js';
|
||||
import { getNow } from '../lib/utils.js';
|
||||
import { eventSeatBreakdownQuery } from '../lib/capacity.js';
|
||||
|
||||
const adminRouter = new Hono();
|
||||
|
||||
@@ -20,8 +21,9 @@ const csvEscape = (value: string) => {
|
||||
adminRouter.get('/dashboard', requireAuth(['admin', 'organizer']), async (c) => {
|
||||
const now = getNow();
|
||||
|
||||
// Get upcoming events
|
||||
const upcomingEvents = await dbAll(
|
||||
// Get upcoming events with seat counts (paid + claimed, per lib/capacity.ts)
|
||||
// so the dashboard's capacity alerts reflect real availability.
|
||||
const upcomingEventsRaw = await dbAll<any>(
|
||||
(db as any)
|
||||
.select()
|
||||
.from(events)
|
||||
@@ -34,6 +36,24 @@ adminRouter.get('/dashboard', requireAuth(['admin', 'organizer']), async (c) =>
|
||||
.orderBy((events as any).startDatetime)
|
||||
.limit(5)
|
||||
);
|
||||
|
||||
const seatRows = await dbAll<any>(eventSeatBreakdownQuery(db));
|
||||
const seatsByEvent = new Map<string, { paid: number; claimed: number }>();
|
||||
for (const row of seatRows) {
|
||||
seatsByEvent.set(row.eventId, {
|
||||
paid: Number(row.paidCount) || 0,
|
||||
claimed: Number(row.claimedCount) || 0,
|
||||
});
|
||||
}
|
||||
const upcomingEvents = upcomingEventsRaw.map((event: any) => {
|
||||
const counts = seatsByEvent.get(event.id) || { paid: 0, claimed: 0 };
|
||||
return {
|
||||
...event,
|
||||
bookedCount: counts.paid,
|
||||
claimedCount: counts.claimed,
|
||||
availableSeats: Math.max(0, (event.capacity || 0) - counts.paid - counts.claimed),
|
||||
};
|
||||
});
|
||||
|
||||
// Get recent tickets
|
||||
const recentTickets = await dbAll(
|
||||
@@ -70,6 +90,8 @@ adminRouter.get('/dashboard', requireAuth(['admin', 'organizer']), async (c) =>
|
||||
.where(eq((tickets as any).status, 'confirmed'))
|
||||
);
|
||||
|
||||
// 'pending' = checkout opened, nothing paid or claimed (informational);
|
||||
// 'pending_approval' = customer says they paid, needs admin verification (actionable).
|
||||
const pendingPayments = await dbGet<any>(
|
||||
(db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
@@ -77,6 +99,13 @@ adminRouter.get('/dashboard', requireAuth(['admin', 'organizer']), async (c) =>
|
||||
.where(eq((payments as any).status, 'pending'))
|
||||
);
|
||||
|
||||
const awaitingApprovalPayments = await dbGet<any>(
|
||||
(db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(payments)
|
||||
.where(eq((payments as any).status, 'pending_approval'))
|
||||
);
|
||||
|
||||
const onHoldPayments = await dbGet<any>(
|
||||
(db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
@@ -115,6 +144,7 @@ adminRouter.get('/dashboard', requireAuth(['admin', 'organizer']), async (c) =>
|
||||
totalTickets: totalTickets?.count || 0,
|
||||
confirmedTickets: confirmedTickets?.count || 0,
|
||||
pendingPayments: pendingPayments?.count || 0,
|
||||
awaitingApprovalPayments: awaitingApprovalPayments?.count || 0,
|
||||
onHoldPayments: onHoldPayments?.count || 0,
|
||||
totalRevenue,
|
||||
newContacts: newContacts?.count || 0,
|
||||
|
||||
Reference in New Issue
Block a user