import express, {Request, Response} from 'express'; import * as RedemptionsAdminService from './redemptions.admin.service'; import {sendServerError} from '../tickets.errors'; export const redemptionsAdminRouter = express.Router(); /** * @swagger * /tickets/admin/redemptions: * get: * summary: List redemptions (admin) * description: Filterable by event and status (ACTIVE/UNDONE). * tags: [tickets-admin] * parameters: * - $ref: '#/components/parameters/SessionIdHeader' * - $ref: '#/components/parameters/SessionKeyHeader' * - in: query * name: eventId * schema: * type: integer * - in: query * name: status * schema: * $ref: '#/components/schemas/RedemptionStatus' * responses: * 200: * description: Success * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/RedemptionSummary' * 401: * description: Unauthorized */ redemptionsAdminRouter.get('/', async (req: Request, res: Response) => { try { const eventId = req.query.eventId !== undefined ? Number(req.query.eventId) : undefined; const status = req.query.status as any; res.status(200).send(await RedemptionsAdminService.listRedemptions({eventId, status})); } catch (e: any) { sendServerError(res, e); } }); /** * @swagger * /tickets/admin/redemptions/{redemptionId}: * get: * summary: Get a single redemption (admin) * tags: [tickets-admin] * parameters: * - $ref: '#/components/parameters/SessionIdHeader' * - $ref: '#/components/parameters/SessionKeyHeader' * - in: path * name: redemptionId * required: true * schema: * type: integer * responses: * 200: * description: Success * 404: * description: Unknown redemption * 401: * description: Unauthorized * patch: * summary: Edit a redemption's contact info and/or guest list * description: Only fields present in the body are changed. Growing the guest count is re-checked against the voucher's max guests and the event's remaining capacity. Logs to the audit trail with an optional admin-supplied reason. * tags: [tickets-admin] * parameters: * - $ref: '#/components/parameters/SessionIdHeader' * - $ref: '#/components/parameters/SessionKeyHeader' * - in: path * name: redemptionId * required: true * schema: * type: integer * requestBody: * content: * application/json: * schema: * type: object * properties: * contactName: * type: string * contactEmail: * type: string * contactAddress: * type: string * nullable: true * guestNames: * type: array * items: * type: string * reason: * type: string * responses: * 200: * description: Edited * 404: * description: Unknown redemption * 409: * description: Not active, exceeds max guests, or exceeds remaining capacity * 401: * description: Unauthorized */ redemptionsAdminRouter.get('/:redemptionId', async (req: Request, res: Response) => { try { const redemption = await RedemptionsAdminService.getRedemption(Number(req.params.redemptionId)); if (!redemption) { res.status(404).send({status: 'NOT_FOUND'}); return; } res.status(200).send(redemption); } catch (e: any) { sendServerError(res, e); } }); redemptionsAdminRouter.patch('/:redemptionId', async (req: Request, res: Response) => { try { const {contactName, contactEmail, contactAddress, guestNames, reason} = req.body || {}; const result = await RedemptionsAdminService.editRedemption( Number(req.params.redemptionId), {contactName, contactEmail, contactAddress, guestNames}, res.locals.admin.email, reason || null ); switch (result.status) { case 'EDITED': res.status(200).send({status: 'OK'}); return; case 'NOT_FOUND': res.status(404).send({status: 'NOT_FOUND'}); return; case 'NOT_ACTIVE': res.status(409).send({status: 'NOT_ACTIVE', message: 'This redemption is not active.'}); return; case 'INVALID_EMAIL': res.status(400).send({status: 'INVALID_EMAIL', message: 'Die E-Mail-Adresse sieht nicht gültig aus.'}); return; case 'EXCEEDS_MAX_GUESTS': res.status(409).send({status: 'EXCEEDS_MAX_GUESTS', maxGuests: result.maxGuests}); return; case 'CAPACITY_EXCEEDED': res.status(409).send({status: 'CAPACITY_EXCEEDED', spotsRemaining: result.spotsRemaining}); return; } } catch (e: any) { sendServerError(res, e); } }); /** * @swagger * /tickets/admin/redemptions/{redemptionId}/undo: * post: * summary: Undo a redemption * description: Reopens the code (back to UNUSED) and marks the redemption UNDONE. Guest data is kept for the audit trail; a later re-redemption creates a new redemption record. * tags: [tickets-admin] * parameters: * - $ref: '#/components/parameters/SessionIdHeader' * - $ref: '#/components/parameters/SessionKeyHeader' * - in: path * name: redemptionId * required: true * schema: * type: integer * requestBody: * content: * application/json: * schema: * type: object * properties: * reason: * type: string * responses: * 200: * description: Undone * 404: * description: Unknown redemption * 409: * description: Redemption is not active * 401: * description: Unauthorized */ redemptionsAdminRouter.post('/:redemptionId/undo', async (req: Request, res: Response) => { try { const result = await RedemptionsAdminService.undoRedemption(Number(req.params.redemptionId), res.locals.admin.email, req.body?.reason || null); if (result === 'NOT_FOUND') { res.status(404).send({status: 'NOT_FOUND'}); return; } if (result === 'NOT_ACTIVE') { res.status(409).send({status: 'NOT_ACTIVE', message: 'This redemption is not active.'}); return; } res.status(200).send({status: 'OK'}); } catch (e: any) { sendServerError(res, e); } }); /** * @swagger * /tickets/admin/vouchers/{code}/history: * get: * summary: Get a voucher's admin-action audit trail * tags: [tickets-admin] * parameters: * - $ref: '#/components/parameters/SessionIdHeader' * - $ref: '#/components/parameters/SessionKeyHeader' * - in: path * name: code * required: true * schema: * type: string * responses: * 200: * description: Success * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/AuditLogEntry' * 401: * description: Unauthorized */ export const voucherHistoryRouter = express.Router(); voucherHistoryRouter.get('/:code/history', async (req: Request, res: Response) => { try { res.status(200).send(await RedemptionsAdminService.getAuditHistory(req.params.code)); } catch (e: any) { sendServerError(res, e); } });