Expose each concert's redemption deadline via the voucher/event APIs #17

Merged
Paddy merged 1 commits from feature/voucher-redemption-deadline into master 2026-09-15 17:32:38 +00:00
3 changed files with 25 additions and 8 deletions
@@ -14,6 +14,7 @@ export interface EventPickerEntry {
startDateTime: Date; startDateTime: Date;
location: string; location: string;
status: string | undefined; status: string | undefined;
redemptionDeadline: Date | null;
} }
/** /**
@@ -26,19 +27,26 @@ export interface EventPickerEntry {
*/ */
export const listEventsForPicker = async (): Promise<EventPickerEntry[]> => { export const listEventsForPicker = async (): Promise<EventPickerEntry[]> => {
let conn = await NachklangTicketsDB.getConnection(); let conn = await NachklangTicketsDB.getConnection();
let enabledEventIds: number[]; let deadlineByEventId: Map<number, Date | null>;
try { try {
const rows = await conn.query('SELECT event_id FROM event_ticket_settings'); const rows = await conn.query('SELECT event_id, redemption_deadline FROM event_ticket_settings');
enabledEventIds = rows.map((r: any) => r.event_id); deadlineByEventId = new Map(rows.map((r: any) => [r.event_id, r.redemption_deadline]));
} finally { } finally {
await conn.end(); await conn.end();
} }
if (enabledEventIds.length === 0) return []; if (deadlineByEventId.size === 0) return [];
const events = await Promise.all(enabledEventIds.map(id => CalendarEventsService.getEventById(id))); const events = await Promise.all([...deadlineByEventId.keys()].map(id => CalendarEventsService.getEventById(id)));
return events return events
.filter((e): e is NonNullable<typeof e> => e !== null && e.status !== 'DELETED') .filter((e): e is NonNullable<typeof e> => e !== null && e.status !== 'DELETED')
.map(e => ({eventId: e.eventId, name: e.name, startDateTime: e.startDateTime, location: e.location, status: e.status})) .map(e => ({
eventId: e.eventId,
name: e.name,
startDateTime: e.startDateTime,
location: e.location,
status: e.status,
redemptionDeadline: deadlineByEventId.get(e.eventId) ?? null
}))
.sort((a, b) => a.startDateTime.getTime() - b.startDateTime.getTime()); .sort((a, b) => a.startDateTime.getTime() - b.startDateTime.getTime());
}; };
@@ -59,7 +67,9 @@ export const listAvailableEventsToAdd = async (): Promise<EventPickerEntry[]> =>
const events = await CalendarEventsService.getAllEventsAdmin(PUBLIC_CALENDAR_ID); const events = await CalendarEventsService.getAllEventsAdmin(PUBLIC_CALENDAR_ID);
return events return events
.filter(e => e.status !== 'DELETED' && !enabledEventIds.has(e.eventId)) .filter(e => e.status !== 'DELETED' && !enabledEventIds.has(e.eventId))
.map(e => ({eventId: e.eventId, name: e.name, startDateTime: e.startDateTime, location: e.location, status: e.status})) // Not yet added to the ticket shop, so there's no event_ticket_settings
// row and therefore no deadline to report.
.map(e => ({eventId: e.eventId, name: e.name, startDateTime: e.startDateTime, location: e.location, status: e.status, redemptionDeadline: null}))
.sort((a, b) => a.startDateTime.getTime() - b.startDateTime.getTime()); .sort((a, b) => a.startDateTime.getTime() - b.startDateTime.getTime());
}; };
@@ -39,6 +39,7 @@ export const validateVoucher = async (code: string): Promise<VoucherValidation |
name: event.name, name: event.name,
startDateTime: event.startDateTime, startDateTime: event.startDateTime,
location: event.location, location: event.location,
redemptionDeadline: ticketState.redemptionDeadline,
deadlinePassed: ticketState.redemptionDeadline !== null && now > new Date(ticketState.redemptionDeadline), deadlinePassed: ticketState.redemptionDeadline !== null && now > new Date(ticketState.redemptionDeadline),
isFull: ticketState.spotsRemaining !== null && ticketState.spotsRemaining <= 0, isFull: ticketState.spotsRemaining !== null && ticketState.spotsRemaining <= 0,
spotsRemaining: ticketState.spotsRemaining, spotsRemaining: ticketState.spotsRemaining,
+7 -1
View File
@@ -10,7 +10,7 @@
* enum: [ACTIVE, UNDONE] * enum: [ACTIVE, UNDONE]
* EligibleEvent: * EligibleEvent:
* type: object * type: object
* required: [eventId, name, startDateTime, location, deadlinePassed, isFull, collectAddress, requireAddress] * required: [eventId, name, startDateTime, location, redemptionDeadline, deadlinePassed, isFull, collectAddress, requireAddress]
* properties: * properties:
* eventId: * eventId:
* type: integer * type: integer
@@ -23,6 +23,11 @@
* format: date-time * format: date-time
* location: * location:
* type: string * type: string
* redemptionDeadline:
* type: string
* format: date-time
* nullable: true
* description: null when the event has no redemption deadline set
* deadlinePassed: * deadlinePassed:
* type: boolean * type: boolean
* isFull: * isFull:
@@ -224,6 +229,7 @@ export interface EligibleEvent {
name: string; name: string;
startDateTime: Date; startDateTime: Date;
location: string; location: string;
redemptionDeadline: Date | null;
deadlinePassed: boolean; deadlinePassed: boolean;
isFull: boolean; isFull: boolean;
spotsRemaining: number | null; spotsRemaining: number | null;