- Add comprehensive analytics components with export functionality - Implement territory management with manager performance tracking - Add seatmap components for venue layout management - Create customer management features with modal interface - Add advanced hooks for dashboard flags and territory data - Implement seat selection and venue management utilities - Add type definitions for ticketing and seatmap systems 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
264 lines
8.8 KiB
JavaScript
264 lines
8.8 KiB
JavaScript
"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
|