Add a per-event "tickets mailed" flag and mention Abendkasse pickup in the confirmation email

New event_ticket_settings.tickets_mailed column (default false, since
no event mails physical tickets today). When false, the redemption
confirmation email tells the guest their tickets await pickup at the
Abendkasse under their name instead. Read directly by
sendRedemptionConfirmation from event_ticket_settings, so both send
paths (redeem flow, admin resend) pick it up without either caller
changing.

Also fixes docker/init/03-tickets-schema.sql, found missing the
SOURCE line for migration 003 while adding 004 - local tickets dev
databases have been silently missing confirmation_email_status.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 19:46:56 +02:00
parent 5b3e10be94
commit e7d76f40de
9 changed files with 106 additions and 13 deletions
@@ -93,7 +93,7 @@ eventsAdminRouter.get('/:eventId/stats', async (req: Request, res: Response) =>
* /tickets/admin/events/{eventId}/settings:
* put:
* summary: Set a concert's voucher settings
* description: Upserts capacity (null = uncapped), redemption deadline (null = none), and whether to collect a mailing address.
* description: Upserts capacity (null = uncapped), redemption deadline (null = none), whether to collect a mailing address, and whether tickets are mailed to guests.
* tags: [tickets-admin]
* security:
* - AdminSessionCookie: []
@@ -122,6 +122,9 @@ eventsAdminRouter.get('/:eventId/stats', async (req: Request, res: Response) =>
* requireAddress:
* type: boolean
* description: Only meaningful when collectAddress is true.
* ticketsMailed:
* type: boolean
* description: When false, the redemption confirmation email tells the guest their tickets await pickup at the Abendkasse instead.
* responses:
* 200:
* description: Saved
@@ -132,7 +135,7 @@ eventsAdminRouter.get('/:eventId/stats', async (req: Request, res: Response) =>
*/
eventsAdminRouter.put('/:eventId/settings', async (req: Request, res: Response) => {
try {
const {capacity, redemptionDeadline, collectAddress, requireAddress} = req.body || {};
const {capacity, redemptionDeadline, collectAddress, requireAddress, ticketsMailed} = req.body || {};
await EventsAdminService.setEventSettings(Number(req.params.eventId), {
capacity: capacity ?? null,
// The mariadb driver needs an actual Date to serialize a DATETIME
@@ -140,7 +143,8 @@ eventsAdminRouter.put('/:eventId/settings', async (req: Request, res: Response)
// rejected with "Incorrect datetime value".
redemptionDeadline: redemptionDeadline ? new Date(redemptionDeadline) : null,
collectAddress: !!collectAddress,
requireAddress: !!collectAddress && !!requireAddress
requireAddress: !!collectAddress && !!requireAddress,
ticketsMailed: !!ticketsMailed
});
res.status(200).send({status: 'OK'});
} catch (e: any) {
@@ -99,6 +99,7 @@ export const getEventStats = async (eventId: number): Promise<EventStats> => {
redemptionDeadline: ticketState.redemptionDeadline,
collectAddress: ticketState.collectAddress,
requireAddress: ticketState.requireAddress,
ticketsMailed: ticketState.ticketsMailed,
guestsUsed: ticketState.guestsUsed,
spotsRemaining: ticketState.spotsRemaining,
unusedCodes,
@@ -119,10 +120,10 @@ export const setEventSettings = async (eventId: number, settings: Omit<EventTick
try {
await conn.beginTransaction();
await conn.query(
`INSERT INTO event_ticket_settings (event_id, capacity, redemption_deadline, collect_address, require_address)
VALUES (?,?,?,?,?)
ON DUPLICATE KEY UPDATE capacity = VALUES(capacity), redemption_deadline = VALUES(redemption_deadline), collect_address = VALUES(collect_address), require_address = VALUES(require_address)`,
[eventId, settings.capacity, settings.redemptionDeadline, settings.collectAddress ? 1 : 0, settings.requireAddress ? 1 : 0]
`INSERT INTO event_ticket_settings (event_id, capacity, redemption_deadline, collect_address, require_address, tickets_mailed)
VALUES (?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE capacity = VALUES(capacity), redemption_deadline = VALUES(redemption_deadline), collect_address = VALUES(collect_address), require_address = VALUES(require_address), tickets_mailed = VALUES(tickets_mailed)`,
[eventId, settings.capacity, settings.redemptionDeadline, settings.collectAddress ? 1 : 0, settings.requireAddress ? 1 : 0, settings.ticketsMailed ? 1 : 0]
);
await conn.commit();
} catch (err) {