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>
This commit is contained in:
264
reactrebuild0825/functions/lib/verify.js
Normal file
264
reactrebuild0825/functions/lib/verify.js
Normal file
@@ -0,0 +1,264 @@
|
||||
"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
|
||||
Reference in New Issue
Block a user