"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.verifyTicket = void 0; const https_1 = require("firebase-functions/v2/https"); const firestore_1 = require("firebase-admin/firestore"); const logger_1 = require("./logger"); const db = (0, firestore_1.getFirestore)(); /** * Core ticket verification logic wrapped with structured logging */ const verifyTicketCore = (0, logger_1.withLogging)("ticket_verification", async (qr, headers) => { const startTime = performance.now(); // Extract context from headers const context = { sessionId: headers['x-scanner-session'], deviceId: headers['x-device-id'], accountId: headers['x-account-id'], orgId: headers['x-org-id'], qr, operation: 'ticket_verification', }; logger_1.logger.addBreadcrumb("Starting ticket verification", "verification", { qr_masked: `${qr.substring(0, 8) }...`, sessionId: context.sessionId, }); // Find ticket by QR code const ticketsSnapshot = await db .collection("tickets") .where("qr", "==", qr) .limit(1) .get(); if (ticketsSnapshot.empty) { const latencyMs = Math.round(performance.now() - startTime); logger_1.logger.logScannerVerify({ ...context, result: 'invalid', reason: 'ticket_not_found', latencyMs, }); return { valid: false, reason: "ticket_not_found", }; } const ticketDoc = ticketsSnapshot.docs[0]; const ticketData = ticketDoc.data(); // Add ticket context const ticketContext = { ...context, eventId: ticketData.eventId, ticketTypeId: ticketData.ticketTypeId, }; logger_1.logger.addBreadcrumb("Ticket found in database", "verification", { ticketId: ticketDoc.id, status: ticketData.status, eventId: ticketData.eventId, }); // Check if already scanned if (ticketData.status === "scanned") { const latencyMs = Math.round(performance.now() - startTime); logger_1.logger.logScannerVerify({ ...ticketContext, result: 'already_scanned', latencyMs, }); return { valid: false, reason: "already_scanned", scannedAt: ticketData.scannedAt?.toDate?.()?.toISOString() || ticketData.scannedAt, ticket: { id: ticketDoc.id, eventId: ticketData.eventId, ticketTypeId: ticketData.ticketTypeId, status: ticketData.status, purchaserEmail: ticketData.purchaserEmail, }, }; } // Check if ticket is void if (ticketData.status === "void") { const latencyMs = Math.round(performance.now() - startTime); logger_1.logger.logScannerVerify({ ...ticketContext, result: 'invalid', reason: 'ticket_voided', latencyMs, }); return { valid: false, reason: "ticket_voided", ticket: { id: ticketDoc.id, eventId: ticketData.eventId, ticketTypeId: ticketData.ticketTypeId, status: ticketData.status, purchaserEmail: ticketData.purchaserEmail, }, }; } // Mark as scanned atomically const scannedAt = new Date(); logger_1.logger.addBreadcrumb("Attempting to mark ticket as scanned", "verification"); try { await db.runTransaction(async (transaction) => { const currentTicket = await transaction.get(ticketDoc.ref); if (!currentTicket.exists) { throw new Error("Ticket was deleted during verification"); } const currentData = currentTicket.data(); // Double-check status hasn't changed if (currentData.status === "scanned") { throw new Error("Ticket was already scanned by another scanner"); } if (currentData.status === "void") { throw new Error("Ticket was voided"); } // Mark as scanned transaction.update(ticketDoc.ref, { status: "scanned", scannedAt, updatedAt: scannedAt, }); }); } catch (transactionError) { // Handle specific transaction errors if (transactionError instanceof Error) { if (transactionError.message.includes("already scanned")) { const latencyMs = Math.round(performance.now() - startTime); logger_1.logger.logScannerVerify({ ...ticketContext, result: 'already_scanned', latencyMs, }); return { valid: false, reason: "already_scanned", }; } if (transactionError.message.includes("voided")) { const latencyMs = Math.round(performance.now() - startTime); logger_1.logger.logScannerVerify({ ...ticketContext, result: 'invalid', reason: 'ticket_voided', latencyMs, }); return { valid: false, reason: "ticket_voided", }; } } // Re-throw for other transaction errors throw transactionError; } // Get additional details for response let eventName = ""; let ticketTypeName = ""; try { const [eventDoc, ticketTypeDoc] = await Promise.all([ db.collection("events").doc(ticketData.eventId).get(), db.collection("ticket_types").doc(ticketData.ticketTypeId).get(), ]); if (eventDoc.exists) { eventName = eventDoc.data().name; } if (ticketTypeDoc.exists) { ticketTypeName = ticketTypeDoc.data().name; } } catch (error) { logger_1.logger.warn("Failed to fetch event/ticket type details", ticketContext, { error: error instanceof Error ? error.message : String(error), ticketId: ticketDoc.id, }); } const latencyMs = Math.round(performance.now() - startTime); logger_1.logger.logScannerVerify({ ...ticketContext, result: 'valid', latencyMs, }); logger_1.logger.addBreadcrumb("Ticket successfully verified and scanned", "verification", { ticketId: ticketDoc.id, eventId: ticketData.eventId, latencyMs, }); return { valid: true, ticket: { id: ticketDoc.id, eventId: ticketData.eventId, ticketTypeId: ticketData.ticketTypeId, eventName, ticketTypeName, status: "scanned", purchaserEmail: ticketData.purchaserEmail, }, }; }, (qr, headers) => ({ qr, sessionId: headers['x-scanner-session'], deviceId: headers['x-device-id'], operation: 'ticket_verification', })); /** * Verifies and marks tickets as scanned * POST /api/tickets/verify * GET /api/tickets/verify/:qr */ exports.verifyTicket = (0, https_1.onRequest)({ cors: true, enforceAppCheck: false, region: "us-central1", }, async (req, res) => { let qr; // Support both POST with body and GET with path parameter if (req.method === "POST") { const {body} = req; qr = body.qr; } else if (req.method === "GET") { // Extract QR from path: /api/tickets/verify/:qr const pathParts = req.path.split("/"); qr = pathParts[pathParts.length - 1]; } else { res.status(405).json({ error: "Method not allowed" }); return; } if (!qr) { logger_1.logger.warn("Verification request missing QR code", { operation: 'ticket_verification', }); res.status(400).json({ valid: false, reason: "QR code is required", }); return; } try { // Extract headers for context const headers = { 'x-scanner-session': req.get('x-scanner-session') || '', 'x-device-id': req.get('x-device-id') || '', 'x-account-id': req.get('x-account-id') || '', 'x-org-id': req.get('x-org-id') || '', }; const response = await verifyTicketCore(qr, headers); res.status(200).json(response); } catch (error) { logger_1.logger.error("Error verifying ticket", error, { qr, operation: 'ticket_verification', }); res.status(500).json({ valid: false, reason: "Internal server error during verification", }); } }); // # sourceMappingURL=verify.js.map