Files
dzinesco aa81eb5adb feat: add advanced analytics and territory management system
- 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>
2025-08-26 09:25:10 -06:00

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