2025-08-13 22:41:37 +01:00
|
|
|
const express = require("express");
|
|
|
|
|
const { v4: uuidv4 } = require("uuid");
|
|
|
|
|
require("dotenv").config();
|
2025-07-29 21:48:34 +01:00
|
|
|
|
|
|
|
|
// Import utilities
|
2025-08-13 22:41:37 +01:00
|
|
|
const redisClient = require("./src/utils/redis-client");
|
|
|
|
|
const fallbackStore = require("./src/utils/fallback-store");
|
|
|
|
|
const logger = require("./src/utils/logger");
|
|
|
|
|
const pdfGenerator = require("./src/utils/pdf-generator");
|
|
|
|
|
const metrics = require("./src/utils/metrics");
|
2025-02-11 14:52:32 +01:00
|
|
|
|
|
|
|
|
const app = express();
|
2025-02-11 15:06:41 +01:00
|
|
|
const port = process.env.PORT || 3049;
|
2025-02-11 14:52:32 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
// Middleware
|
2025-02-11 14:52:32 +01:00
|
|
|
app.use(express.json());
|
2025-08-13 22:41:37 +01:00
|
|
|
app.use(express.static("public"));
|
2025-07-29 21:48:34 +01:00
|
|
|
|
|
|
|
|
// Request logging middleware
|
|
|
|
|
app.use((req, res, next) => {
|
|
|
|
|
const start = Date.now();
|
2025-08-13 22:41:37 +01:00
|
|
|
res.on("finish", () => {
|
2025-07-29 21:48:34 +01:00
|
|
|
const responseTime = Date.now() - start;
|
|
|
|
|
logger.logRequest(req, res, responseTime);
|
|
|
|
|
});
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-13 22:41:37 +01:00
|
|
|
// Prometheus metrics middleware
|
|
|
|
|
app.use(metrics.metricsMiddleware);
|
2025-07-29 21:48:34 +01:00
|
|
|
|
|
|
|
|
// Health check endpoint
|
2025-08-13 22:41:37 +01:00
|
|
|
app.get("/health", async (req, res) => {
|
2025-07-29 21:48:34 +01:00
|
|
|
const redisHealthy = redisClient.isHealthy();
|
|
|
|
|
const fallbackActive = fallbackStore.isActive;
|
2025-08-13 22:41:37 +01:00
|
|
|
|
|
|
|
|
// Update metrics
|
|
|
|
|
metrics.updateRedisStatus(redisHealthy);
|
|
|
|
|
metrics.updateFallbackStatus(fallbackActive);
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
res.json({
|
2025-08-13 22:41:37 +01:00
|
|
|
status: "ok",
|
2025-07-29 21:48:34 +01:00
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
redis: {
|
|
|
|
|
connected: redisHealthy,
|
2025-08-13 22:41:37 +01:00
|
|
|
fallbackActive: fallbackActive,
|
2025-07-29 21:48:34 +01:00
|
|
|
},
|
2025-08-13 22:41:37 +01:00
|
|
|
uptime: process.uptime(),
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Get all events endpoint
|
2025-08-13 22:41:37 +01:00
|
|
|
app.get("/events", async (req, res) => {
|
2025-07-29 21:48:34 +01:00
|
|
|
try {
|
|
|
|
|
let events;
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
if (redisClient.isHealthy()) {
|
|
|
|
|
events = await redisClient.getAllEvents();
|
|
|
|
|
} else {
|
|
|
|
|
events = fallbackStore.getAllEvents();
|
|
|
|
|
}
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
events,
|
2025-08-13 22:41:37 +01:00
|
|
|
usingFallback: fallbackStore.isActive,
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
|
|
|
|
} catch (error) {
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.error("Error fetching events:", error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "Failed to fetch events",
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Get specific event stats
|
2025-08-13 22:41:37 +01:00
|
|
|
app.get("/events/:eventId", async (req, res) => {
|
2025-07-29 21:48:34 +01:00
|
|
|
try {
|
|
|
|
|
const eventId = req.params.eventId;
|
|
|
|
|
let eventStats;
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
if (redisClient.isHealthy()) {
|
|
|
|
|
eventStats = await redisClient.getEventStats(eventId);
|
|
|
|
|
} else {
|
|
|
|
|
eventStats = fallbackStore.getEventStats(eventId);
|
|
|
|
|
}
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
if (!eventStats) {
|
2025-08-13 22:41:37 +01:00
|
|
|
return res.status(404).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "Event not found",
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
|
|
|
|
}
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
event: eventStats,
|
2025-08-13 22:41:37 +01:00
|
|
|
usingFallback: fallbackStore.isActive,
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
|
|
|
|
} catch (error) {
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.error("Error fetching event stats:", error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "Failed to fetch event stats",
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-02-11 14:52:32 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
// Purchase ticket endpoint (multi-event)
|
2025-08-13 22:41:37 +01:00
|
|
|
app.post("/buy/:eventId", async (req, res) => {
|
2025-07-29 21:48:34 +01:00
|
|
|
const startTime = Date.now();
|
|
|
|
|
const eventId = req.params.eventId;
|
|
|
|
|
const purchaseId = uuidv4();
|
|
|
|
|
const timestamp = new Date().toISOString();
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
try {
|
|
|
|
|
let result;
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
// Try Redis first
|
|
|
|
|
if (redisClient.isHealthy()) {
|
|
|
|
|
try {
|
2025-08-13 22:41:37 +01:00
|
|
|
const luaResult = await redisClient.purchaseTicket(
|
|
|
|
|
eventId,
|
|
|
|
|
purchaseId,
|
|
|
|
|
timestamp
|
|
|
|
|
);
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
if (luaResult[0]) {
|
|
|
|
|
// Success - generate PDF ticket
|
|
|
|
|
try {
|
|
|
|
|
// Get event details for PDF
|
|
|
|
|
const eventStats = await redisClient.getEventStats(eventId);
|
2025-08-13 22:41:37 +01:00
|
|
|
|
|
|
|
|
const pdfStartTime = Date.now();
|
2025-07-29 21:48:34 +01:00
|
|
|
const pdfResult = await pdfGenerator.generateTicketPDF({
|
|
|
|
|
ticketId: luaResult[0],
|
|
|
|
|
eventId,
|
|
|
|
|
purchaseId,
|
|
|
|
|
eventName: eventStats?.name || `Event ${eventId}`,
|
2025-08-13 22:41:37 +01:00
|
|
|
eventDescription:
|
|
|
|
|
eventStats?.description || "Event description not available",
|
2025-07-29 21:48:34 +01:00
|
|
|
timestamp,
|
2025-08-13 22:41:37 +01:00
|
|
|
soldCount: luaResult[2],
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
2025-08-13 22:41:37 +01:00
|
|
|
|
|
|
|
|
// Record PDF generation metrics
|
|
|
|
|
const pdfDuration = (Date.now() - pdfStartTime) / 1000;
|
|
|
|
|
metrics.recordPDFGeneration(
|
|
|
|
|
pdfResult.success ? "success" : "failed",
|
|
|
|
|
pdfDuration
|
|
|
|
|
);
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
result = {
|
|
|
|
|
success: true,
|
|
|
|
|
ticket: luaResult[0],
|
|
|
|
|
purchaseId,
|
|
|
|
|
eventId,
|
|
|
|
|
soldCount: luaResult[2],
|
2025-08-13 22:41:37 +01:00
|
|
|
message: "Ticket purchased successfully!",
|
2025-07-29 21:48:34 +01:00
|
|
|
usingFallback: false,
|
|
|
|
|
pdf: {
|
|
|
|
|
generated: pdfResult.success,
|
|
|
|
|
filename: pdfResult.filename,
|
2025-08-13 22:41:37 +01:00
|
|
|
downloadUrl: `/tickets/${purchaseId}`,
|
|
|
|
|
},
|
2025-07-29 21:48:34 +01:00
|
|
|
};
|
2025-08-13 22:41:37 +01:00
|
|
|
|
|
|
|
|
// Record metrics for successful purchase
|
|
|
|
|
metrics.recordTicketSale(eventId, "success");
|
|
|
|
|
metrics.updateTicketMetrics(
|
|
|
|
|
eventId,
|
|
|
|
|
luaResult[2],
|
|
|
|
|
luaResult[3] || 0
|
|
|
|
|
);
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
logger.logPurchase(eventId, luaResult[0], purchaseId, true);
|
|
|
|
|
logger.info(`PDF ticket generated for purchase ${purchaseId}`);
|
|
|
|
|
} catch (pdfError) {
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.error("PDF generation failed:", pdfError);
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
// Still return success for ticket purchase, but note PDF failure
|
|
|
|
|
result = {
|
|
|
|
|
success: true,
|
|
|
|
|
ticket: luaResult[0],
|
|
|
|
|
purchaseId,
|
|
|
|
|
eventId,
|
|
|
|
|
soldCount: luaResult[2],
|
2025-08-13 22:41:37 +01:00
|
|
|
message: "Ticket purchased successfully! (PDF generation failed)",
|
2025-07-29 21:48:34 +01:00
|
|
|
usingFallback: false,
|
|
|
|
|
pdf: {
|
|
|
|
|
generated: false,
|
2025-08-13 22:41:37 +01:00
|
|
|
error: "PDF generation failed",
|
|
|
|
|
},
|
2025-07-29 21:48:34 +01:00
|
|
|
};
|
2025-08-13 22:41:37 +01:00
|
|
|
|
|
|
|
|
// Record metrics for successful purchase (even with PDF failure)
|
|
|
|
|
metrics.recordTicketSale(eventId, "success");
|
|
|
|
|
metrics.updateTicketMetrics(
|
|
|
|
|
eventId,
|
|
|
|
|
luaResult[2],
|
|
|
|
|
luaResult[3] || 0
|
|
|
|
|
);
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
logger.logPurchase(eventId, luaResult[0], purchaseId, true);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Failed - handle specific error
|
|
|
|
|
const errorCode = luaResult[1];
|
|
|
|
|
let statusCode = 400;
|
2025-08-13 22:41:37 +01:00
|
|
|
let message = "Purchase failed";
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
switch (errorCode) {
|
2025-08-13 22:41:37 +01:00
|
|
|
case "EVENT_NOT_FOUND":
|
2025-07-29 21:48:34 +01:00
|
|
|
statusCode = 404;
|
2025-08-13 22:41:37 +01:00
|
|
|
message = "Event not found";
|
2025-07-29 21:48:34 +01:00
|
|
|
break;
|
2025-08-13 22:41:37 +01:00
|
|
|
case "NO_TICKETS_AVAILABLE":
|
2025-07-29 21:48:34 +01:00
|
|
|
statusCode = 409;
|
2025-08-13 22:41:37 +01:00
|
|
|
message = "No tickets available for this event";
|
2025-07-29 21:48:34 +01:00
|
|
|
break;
|
|
|
|
|
}
|
2025-08-13 22:41:37 +01:00
|
|
|
|
|
|
|
|
// Record metrics for failed purchase
|
|
|
|
|
metrics.recordTicketSale(eventId, "failed");
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
logger.logPurchase(eventId, null, purchaseId, false, errorCode);
|
2025-08-13 22:41:37 +01:00
|
|
|
return res.status(statusCode).json({
|
|
|
|
|
success: false,
|
2025-07-29 21:48:34 +01:00
|
|
|
message,
|
|
|
|
|
errorCode,
|
|
|
|
|
eventId,
|
2025-08-13 22:41:37 +01:00
|
|
|
purchaseId,
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (redisError) {
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.error("Redis purchase failed, attempting fallback:", redisError);
|
2025-07-29 21:48:34 +01:00
|
|
|
// Activate fallback if not already active
|
|
|
|
|
if (!fallbackStore.isActive) {
|
2025-08-13 22:41:37 +01:00
|
|
|
fallbackStore.activate("Redis purchase operation failed");
|
|
|
|
|
// Try to sync with Redis data if possible
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (fallbackStore.isActive && fallbackStore.events.size === 0) {
|
|
|
|
|
fallbackStore.attemptReseed();
|
|
|
|
|
}
|
|
|
|
|
}, 100);
|
2025-07-29 21:48:34 +01:00
|
|
|
}
|
|
|
|
|
throw redisError; // Will be caught by outer try-catch for fallback
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-08-13 22:41:37 +01:00
|
|
|
throw new Error("Redis not available");
|
2025-07-29 21:48:34 +01:00
|
|
|
}
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
const responseTime = Date.now() - startTime;
|
|
|
|
|
result.responseTime = `${responseTime}ms`;
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
res.json(result);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Fallback to in-memory store
|
|
|
|
|
try {
|
|
|
|
|
if (!fallbackStore.isActive) {
|
2025-08-13 22:41:37 +01:00
|
|
|
fallbackStore.activate("Redis connection failed during purchase");
|
|
|
|
|
// Try to sync with Redis data if possible
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (fallbackStore.isActive && fallbackStore.events.size === 0) {
|
|
|
|
|
fallbackStore.attemptReseed();
|
|
|
|
|
}
|
|
|
|
|
}, 100);
|
2025-07-29 21:48:34 +01:00
|
|
|
}
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
const fallbackResult = fallbackStore.purchaseTicket(eventId, purchaseId);
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
if (fallbackResult.success) {
|
|
|
|
|
// Generate PDF for fallback purchase
|
|
|
|
|
try {
|
|
|
|
|
const eventStats = fallbackStore.getEventStats(eventId);
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
const pdfResult = await pdfGenerator.generateTicketPDF({
|
|
|
|
|
ticketId: fallbackResult.ticket,
|
|
|
|
|
eventId,
|
|
|
|
|
purchaseId,
|
|
|
|
|
eventName: eventStats?.name || `Event ${eventId}`,
|
2025-08-13 22:41:37 +01:00
|
|
|
eventDescription:
|
|
|
|
|
eventStats?.description || "Event description not available",
|
2025-07-29 21:48:34 +01:00
|
|
|
timestamp,
|
2025-08-13 22:41:37 +01:00
|
|
|
soldCount: fallbackResult.soldCount,
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
const responseTime = Date.now() - startTime;
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
ticket: fallbackResult.ticket,
|
|
|
|
|
purchaseId,
|
|
|
|
|
eventId,
|
|
|
|
|
soldCount: fallbackResult.soldCount,
|
2025-08-13 22:41:37 +01:00
|
|
|
message: "Ticket purchased successfully (fallback mode)!",
|
2025-07-29 21:48:34 +01:00
|
|
|
usingFallback: true,
|
|
|
|
|
responseTime: `${responseTime}ms`,
|
|
|
|
|
pdf: {
|
|
|
|
|
generated: pdfResult.success,
|
|
|
|
|
filename: pdfResult.filename,
|
2025-08-13 22:41:37 +01:00
|
|
|
downloadUrl: `/tickets/${purchaseId}`,
|
|
|
|
|
},
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
2025-08-13 22:41:37 +01:00
|
|
|
|
|
|
|
|
// Record metrics for successful fallback purchase
|
|
|
|
|
metrics.recordTicketSale(eventId, "success_fallback");
|
|
|
|
|
metrics.updateTicketMetrics(
|
|
|
|
|
eventId,
|
|
|
|
|
fallbackResult.soldCount,
|
|
|
|
|
fallbackResult.remainingTickets || 0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
`PDF ticket generated for fallback purchase ${purchaseId}`
|
|
|
|
|
);
|
2025-07-29 21:48:34 +01:00
|
|
|
} catch (pdfError) {
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.error("PDF generation failed in fallback mode:", pdfError);
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
const responseTime = Date.now() - startTime;
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
ticket: fallbackResult.ticket,
|
|
|
|
|
purchaseId,
|
|
|
|
|
eventId,
|
|
|
|
|
soldCount: fallbackResult.soldCount,
|
2025-08-13 22:41:37 +01:00
|
|
|
message:
|
|
|
|
|
"Ticket purchased successfully (fallback mode, PDF generation failed)!",
|
2025-07-29 21:48:34 +01:00
|
|
|
usingFallback: true,
|
|
|
|
|
responseTime: `${responseTime}ms`,
|
|
|
|
|
pdf: {
|
|
|
|
|
generated: false,
|
2025-08-13 22:41:37 +01:00
|
|
|
error: "PDF generation failed",
|
|
|
|
|
},
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
2025-08-13 22:41:37 +01:00
|
|
|
|
|
|
|
|
// Record metrics for successful fallback purchase (even with PDF failure)
|
|
|
|
|
metrics.recordTicketSale(eventId, "success_fallback");
|
|
|
|
|
metrics.updateTicketMetrics(
|
|
|
|
|
eventId,
|
|
|
|
|
fallbackResult.soldCount,
|
|
|
|
|
fallbackResult.remainingTickets || 0
|
|
|
|
|
);
|
2025-07-29 21:48:34 +01:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let statusCode = 400;
|
2025-08-13 22:41:37 +01:00
|
|
|
let message = "Purchase failed";
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
switch (fallbackResult.error) {
|
2025-08-13 22:41:37 +01:00
|
|
|
case "EVENT_NOT_FOUND":
|
2025-07-29 21:48:34 +01:00
|
|
|
statusCode = 404;
|
2025-08-13 22:41:37 +01:00
|
|
|
message = "Event not found";
|
2025-07-29 21:48:34 +01:00
|
|
|
break;
|
2025-08-13 22:41:37 +01:00
|
|
|
case "NO_TICKETS_AVAILABLE":
|
2025-07-29 21:48:34 +01:00
|
|
|
statusCode = 409;
|
2025-08-13 22:41:37 +01:00
|
|
|
message = "No tickets available for this event";
|
2025-07-29 21:48:34 +01:00
|
|
|
break;
|
|
|
|
|
}
|
2025-08-13 22:41:37 +01:00
|
|
|
|
|
|
|
|
// Record metrics for failed fallback purchase
|
|
|
|
|
metrics.recordTicketSale(eventId, "failed_fallback");
|
|
|
|
|
|
|
|
|
|
logger.logPurchase(
|
|
|
|
|
eventId,
|
|
|
|
|
null,
|
|
|
|
|
purchaseId,
|
|
|
|
|
false,
|
|
|
|
|
fallbackResult.error
|
|
|
|
|
);
|
|
|
|
|
res.status(statusCode).json({
|
|
|
|
|
success: false,
|
2025-07-29 21:48:34 +01:00
|
|
|
message,
|
|
|
|
|
errorCode: fallbackResult.error,
|
|
|
|
|
eventId,
|
|
|
|
|
purchaseId,
|
2025-08-13 22:41:37 +01:00
|
|
|
usingFallback: true,
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (fallbackError) {
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.error("Both Redis and fallback failed:", fallbackError);
|
|
|
|
|
|
|
|
|
|
// Record metrics for system failure
|
|
|
|
|
metrics.recordTicketSale(eventId, "system_error");
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
logger.logPurchase(eventId, null, purchaseId, false, fallbackError);
|
2025-08-13 22:41:37 +01:00
|
|
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "System temporarily unavailable",
|
2025-07-29 21:48:34 +01:00
|
|
|
eventId,
|
2025-08-13 22:41:37 +01:00
|
|
|
purchaseId,
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-02-11 14:52:32 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
// Download ticket PDF endpoint
|
2025-08-13 22:41:37 +01:00
|
|
|
app.get("/tickets/:purchaseId", async (req, res) => {
|
2025-02-11 14:52:32 +01:00
|
|
|
try {
|
2025-07-29 21:48:34 +01:00
|
|
|
const purchaseId = req.params.purchaseId;
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
if (!pdfGenerator.ticketExists(purchaseId)) {
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
success: false,
|
2025-08-13 22:41:37 +01:00
|
|
|
message: "Ticket not found",
|
2025-02-11 14:52:32 +01:00
|
|
|
});
|
2025-07-29 21:48:34 +01:00
|
|
|
}
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
const filepath = pdfGenerator.getTicketPath(purchaseId);
|
|
|
|
|
const filename = `ticket-${purchaseId}.pdf`;
|
2025-08-13 22:41:37 +01:00
|
|
|
|
|
|
|
|
res.setHeader("Content-Type", "application/pdf");
|
|
|
|
|
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
|
|
|
|
|
|
|
|
|
const fileStream = require("fs").createReadStream(filepath);
|
2025-07-29 21:48:34 +01:00
|
|
|
fileStream.pipe(res);
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
logger.info(`PDF ticket downloaded: ${purchaseId}`);
|
|
|
|
|
} catch (error) {
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.error("Error downloading ticket:", error);
|
2025-07-29 21:48:34 +01:00
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
2025-08-13 22:41:37 +01:00
|
|
|
message: "Failed to download ticket",
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// PDF management endpoint
|
2025-08-13 22:41:37 +01:00
|
|
|
app.get("/admin/pdf-stats", async (req, res) => {
|
2025-07-29 21:48:34 +01:00
|
|
|
try {
|
|
|
|
|
const stats = pdfGenerator.getStats();
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
2025-08-13 22:41:37 +01:00
|
|
|
stats,
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
|
|
|
|
} catch (error) {
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.error("Error getting PDF stats:", error);
|
2025-07-29 21:48:34 +01:00
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
2025-08-13 22:41:37 +01:00
|
|
|
message: "Failed to get PDF statistics",
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Cleanup old tickets endpoint
|
2025-08-13 22:41:37 +01:00
|
|
|
app.post("/admin/cleanup-tickets", async (req, res) => {
|
2025-07-29 21:48:34 +01:00
|
|
|
try {
|
|
|
|
|
const maxAgeHours = req.body.maxAgeHours || 24;
|
|
|
|
|
const deletedCount = await pdfGenerator.cleanupOldTickets(maxAgeHours);
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
message: `Cleaned up ${deletedCount} old tickets`,
|
2025-08-13 22:41:37 +01:00
|
|
|
deletedCount,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error("Error cleaning up tickets:", error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "Failed to cleanup tickets",
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
2025-08-13 22:41:37 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Seed fallback store endpoint
|
|
|
|
|
app.post("/admin/seed-fallback", async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
if (redisClient.isHealthy()) {
|
|
|
|
|
// Activate fallback store temporarily for seeding
|
|
|
|
|
fallbackStore.activate("Manual seeding from admin endpoint");
|
|
|
|
|
|
|
|
|
|
// Get all events from Redis and seed fallback store
|
|
|
|
|
const events = await redisClient.getAllEvents();
|
|
|
|
|
const globalStats = await redisClient.getGlobalStats();
|
|
|
|
|
|
|
|
|
|
for (const event of events) {
|
|
|
|
|
// Get remaining tickets for this event
|
|
|
|
|
const remainingTickets = await redisClient.getRemainingTickets(
|
|
|
|
|
event.eventId
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Create metadata object
|
|
|
|
|
const metadata = {
|
|
|
|
|
eventId: event.eventId,
|
|
|
|
|
totalTickets: event.totalTickets,
|
|
|
|
|
soldTickets: event.soldTickets,
|
|
|
|
|
createdAt: event.createdAt,
|
|
|
|
|
name: event.name,
|
|
|
|
|
description: event.description,
|
|
|
|
|
lastSoldAt: event.lastSoldAt,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Seed the event in fallback store
|
|
|
|
|
fallbackStore.seedEvent(event.eventId, remainingTickets, metadata);
|
|
|
|
|
|
|
|
|
|
// Update sold tickets count
|
|
|
|
|
const fallbackEvent = fallbackStore.events.get(event.eventId);
|
|
|
|
|
if (fallbackEvent) {
|
|
|
|
|
fallbackEvent.soldTickets = event.soldTickets;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update global stats
|
|
|
|
|
if (globalStats) {
|
|
|
|
|
fallbackStore.globalStats.totalSold = globalStats.totalSold;
|
|
|
|
|
fallbackStore.globalStats.lastSeeded = new Date().toISOString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Deactivate fallback store (will be activated when needed)
|
|
|
|
|
fallbackStore.deactivate();
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
message: `Fallback store seeded with ${events.length} events`,
|
|
|
|
|
eventsCount: events.length,
|
|
|
|
|
totalTickets: globalStats?.totalTickets || 0,
|
|
|
|
|
totalSold: globalStats?.totalSold || 0,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
res.status(503).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "Redis not available - cannot seed fallback store",
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-07-29 21:48:34 +01:00
|
|
|
} catch (error) {
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.error("Error seeding fallback store:", error);
|
2025-07-29 21:48:34 +01:00
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
2025-08-13 22:41:37 +01:00
|
|
|
message: "Failed to seed fallback store",
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Metrics endpoint (Prometheus compatible)
|
2025-08-13 22:41:37 +01:00
|
|
|
app.get("/metrics", async (req, res) => {
|
2025-07-29 21:48:34 +01:00
|
|
|
try {
|
|
|
|
|
let globalStats, events;
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
if (redisClient.isHealthy()) {
|
|
|
|
|
globalStats = await redisClient.getGlobalStats();
|
|
|
|
|
events = await redisClient.getAllEvents();
|
2025-02-11 14:52:32 +01:00
|
|
|
} else {
|
2025-07-29 21:48:34 +01:00
|
|
|
globalStats = fallbackStore.getGlobalStats();
|
|
|
|
|
events = fallbackStore.getAllEvents();
|
2025-02-11 14:52:32 +01:00
|
|
|
}
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
// Get PDF stats
|
|
|
|
|
const pdfStats = pdfGenerator.getStats();
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
// Calculate metrics
|
|
|
|
|
const metrics = {
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
global: globalStats,
|
|
|
|
|
events: events,
|
|
|
|
|
system: {
|
|
|
|
|
usingFallback: fallbackStore.isActive,
|
|
|
|
|
redisConnected: redisClient.isHealthy(),
|
|
|
|
|
uptime: process.uptime(),
|
2025-08-13 22:41:37 +01:00
|
|
|
memoryUsage: process.memoryUsage(),
|
2025-07-29 21:48:34 +01:00
|
|
|
},
|
2025-08-13 22:41:37 +01:00
|
|
|
pdf: pdfStats,
|
2025-07-29 21:48:34 +01:00
|
|
|
};
|
2025-08-13 22:41:37 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
res.json(metrics);
|
2025-02-11 14:52:32 +01:00
|
|
|
} catch (error) {
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.error("Error generating metrics:", error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "Failed to generate metrics",
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
2025-02-11 14:52:32 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
// Initialize server
|
|
|
|
|
async function initializeServer() {
|
|
|
|
|
try {
|
|
|
|
|
// Connect to Redis
|
|
|
|
|
await redisClient.connect();
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.info("Redis connected successfully");
|
|
|
|
|
|
|
|
|
|
// Ensure fallback store is seeded with current Redis data
|
|
|
|
|
await ensureFallbackStoreSeeded();
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
// Start server
|
|
|
|
|
app.listen(port, () => {
|
|
|
|
|
logger.info(`🚀 Ticket Microservice running on port ${port}`);
|
|
|
|
|
logger.info(`📊 Health check: http://localhost:${port}/health`);
|
|
|
|
|
logger.info(`📈 Metrics: http://localhost:${port}/metrics`);
|
|
|
|
|
logger.info(`🎫 Events: http://localhost:${port}/events`);
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.error("Failed to initialize server:", error);
|
|
|
|
|
logger.warn("Starting server with fallback store only");
|
|
|
|
|
|
|
|
|
|
fallbackStore.activate("Redis connection failed at startup");
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
app.listen(port, () => {
|
|
|
|
|
logger.warn(`⚠️ Server running in FALLBACK MODE on port ${port}`);
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.warn("Redis connection failed - using in-memory store");
|
2025-07-29 21:48:34 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-13 22:41:37 +01:00
|
|
|
// Ensure fallback store is seeded with current Redis data
|
|
|
|
|
async function ensureFallbackStoreSeeded() {
|
|
|
|
|
try {
|
|
|
|
|
// Check if fallback store needs seeding
|
|
|
|
|
if (fallbackStore.events.size === 0) {
|
|
|
|
|
logger.info(
|
|
|
|
|
"Fallback store is empty, seeding with current Redis data..."
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Temporarily activate fallback store for seeding
|
|
|
|
|
fallbackStore.activate("Seeding during server initialization");
|
|
|
|
|
|
|
|
|
|
// Get all events from Redis and seed fallback store
|
|
|
|
|
const events = await redisClient.getAllEvents();
|
|
|
|
|
const globalStats = await redisClient.getGlobalStats();
|
|
|
|
|
|
|
|
|
|
for (const event of events) {
|
|
|
|
|
// Get remaining tickets for this event
|
|
|
|
|
const remainingTickets = await redisClient.getRemainingTickets(
|
|
|
|
|
event.eventId
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Create metadata object
|
|
|
|
|
const metadata = {
|
|
|
|
|
eventId: event.eventId,
|
|
|
|
|
totalTickets: event.totalTickets,
|
|
|
|
|
soldTickets: event.soldTickets,
|
|
|
|
|
createdAt: event.createdAt,
|
|
|
|
|
name: event.name,
|
|
|
|
|
description: event.description,
|
|
|
|
|
lastSoldAt: event.lastSoldAt,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Seed the event in fallback store
|
|
|
|
|
fallbackStore.seedEvent(event.eventId, remainingTickets, metadata);
|
|
|
|
|
|
|
|
|
|
// Update sold tickets count
|
|
|
|
|
const fallbackEvent = fallbackStore.events.get(event.eventId);
|
|
|
|
|
if (fallbackEvent) {
|
|
|
|
|
fallbackEvent.soldTickets = event.soldTickets;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update global stats
|
|
|
|
|
if (globalStats) {
|
|
|
|
|
fallbackStore.globalStats.totalSold = globalStats.totalSold;
|
|
|
|
|
fallbackStore.globalStats.lastSeeded = new Date().toISOString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info(`Fallback store seeded with ${events.length} events`);
|
|
|
|
|
|
|
|
|
|
// Deactivate fallback store (will be activated when needed)
|
|
|
|
|
fallbackStore.deactivate();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error("Error seeding fallback store during initialization:", error);
|
|
|
|
|
// Don't fail server startup if fallback seeding fails
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Global error handler
|
|
|
|
|
app.use((err, req, res, next) => {
|
|
|
|
|
logger.error("Unhandled error:", err);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: "Internal Server Error",
|
|
|
|
|
error: process.env.NODE_ENV === "development" ? err.message : undefined,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
// Graceful shutdown
|
2025-08-13 22:41:37 +01:00
|
|
|
process.on("SIGINT", async () => {
|
|
|
|
|
logger.info("Received SIGINT, shutting down gracefully...");
|
2025-07-29 21:48:34 +01:00
|
|
|
try {
|
|
|
|
|
await redisClient.disconnect();
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.info("Redis disconnected");
|
2025-07-29 21:48:34 +01:00
|
|
|
} catch (error) {
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.error("Error disconnecting Redis:", error);
|
2025-07-29 21:48:34 +01:00
|
|
|
}
|
|
|
|
|
process.exit(0);
|
2025-02-11 14:52:32 +01:00
|
|
|
});
|
2025-07-29 21:48:34 +01:00
|
|
|
|
2025-08-13 22:41:37 +01:00
|
|
|
process.on("SIGTERM", async () => {
|
|
|
|
|
logger.info("Received SIGTERM, shutting down gracefully...");
|
2025-07-29 21:48:34 +01:00
|
|
|
try {
|
|
|
|
|
await redisClient.disconnect();
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.info("Redis disconnected");
|
2025-07-29 21:48:34 +01:00
|
|
|
} catch (error) {
|
2025-08-13 22:41:37 +01:00
|
|
|
logger.error("Error disconnecting Redis:", error);
|
2025-07-29 21:48:34 +01:00
|
|
|
}
|
|
|
|
|
process.exit(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Start the server
|
|
|
|
|
initializeServer();
|