export interface EventTicketState { eventId: number; capacity: number | null; redemptionDeadline: Date | null; collectAddress: boolean; requireAddress: boolean; guestsUsed: number; spotsRemaining: number | null; } /** * Reads an event's voucher settings + live guest count within the caller's * connection/transaction. Absence of a settings row means uncapped/no * deadline/no address collection - the "absence over sentinels" convention * also used by the Feedback module. * * Pass forUpdate=true from inside the redeem transaction to lock the * settings row for the duration of that transaction, serializing concurrent * redemptions against the same event so the guestsUsed sum computed here * stays correct even under a last-spot race. Events with no settings row * (uncapped) don't need this - there's no cap to race against. */ export const getEventTicketState = async (conn: any, eventId: number, forUpdate = false): Promise => { const settingsQuery = `SELECT capacity, redemption_deadline, collect_address, require_address FROM event_ticket_settings WHERE event_id = ?${forUpdate ? ' FOR UPDATE' : ''}`; const settingsRows = await conn.query(settingsQuery, [eventId]); const capacity = settingsRows.length > 0 ? settingsRows[0].capacity : null; const redemptionDeadline = settingsRows.length > 0 ? settingsRows[0].redemption_deadline : null; const collectAddress = settingsRows.length > 0 ? !!settingsRows[0].collect_address : false; // Only meaningful when collectAddress is also true - the field isn't // shown/collected at all otherwise, so "required" is moot. const requireAddress = collectAddress && settingsRows.length > 0 ? !!settingsRows[0].require_address : false; const usedRows = await conn.query( "SELECT COALESCE(SUM(guest_count), 0) as used FROM redemptions WHERE event_id = ? AND status = 'ACTIVE'", [eventId] ); const guestsUsed = Number(usedRows[0].used); const spotsRemaining = capacity === null ? null : Math.max(0, capacity - guestsUsed); return {eventId, capacity, redemptionDeadline, collectAddress, requireAddress, guestsUsed, spotsRemaining}; };