2025-07-29 21:48:34 +01:00
|
|
|
const express = require('express');
|
|
|
|
|
const { v4: uuidv4 } = require('uuid');
|
|
|
|
|
require('dotenv').config();
|
|
|
|
|
|
|
|
|
|
// Import utilities
|
|
|
|
|
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');
|
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-07-29 21:48:34 +01:00
|
|
|
app.use(express.static('public'));
|
|
|
|
|
|
|
|
|
|
// Request logging middleware
|
|
|
|
|
app.use((req, res, next) => {
|
|
|
|
|
const start = Date.now();
|
|
|
|
|
res.on('finish', () => {
|
|
|
|
|
const responseTime = Date.now() - start;
|
|
|
|
|
logger.logRequest(req, res, responseTime);
|
|
|
|
|
});
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Health check endpoint
|
|
|
|
|
app.get('/health', async (req, res) => {
|
|
|
|
|
const redisHealthy = redisClient.isHealthy();
|
|
|
|
|
const fallbackActive = fallbackStore.isActive;
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
status: 'ok',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
redis: {
|
|
|
|
|
connected: redisHealthy,
|
|
|
|
|
fallbackActive: fallbackActive
|
|
|
|
|
},
|
|
|
|
|
uptime: process.uptime()
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Get all events endpoint
|
|
|
|
|
app.get('/events', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
let events;
|
|
|
|
|
|
|
|
|
|
if (redisClient.isHealthy()) {
|
|
|
|
|
events = await redisClient.getAllEvents();
|
|
|
|
|
} else {
|
|
|
|
|
events = fallbackStore.getAllEvents();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
events,
|
|
|
|
|
usingFallback: fallbackStore.isActive
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Error fetching events:', error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: 'Failed to fetch events'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Get specific event stats
|
|
|
|
|
app.get('/events/:eventId', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const eventId = req.params.eventId;
|
|
|
|
|
let eventStats;
|
|
|
|
|
|
|
|
|
|
if (redisClient.isHealthy()) {
|
|
|
|
|
eventStats = await redisClient.getEventStats(eventId);
|
|
|
|
|
} else {
|
|
|
|
|
eventStats = fallbackStore.getEventStats(eventId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!eventStats) {
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: 'Event not found'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
event: eventStats,
|
|
|
|
|
usingFallback: fallbackStore.isActive
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Error fetching event stats:', error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: 'Failed to fetch event stats'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-02-11 14:52:32 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
// Purchase ticket endpoint (multi-event)
|
|
|
|
|
app.post('/buy/:eventId', async (req, res) => {
|
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
const eventId = req.params.eventId;
|
|
|
|
|
const purchaseId = uuidv4();
|
|
|
|
|
const timestamp = new Date().toISOString();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let result;
|
|
|
|
|
|
|
|
|
|
// Try Redis first
|
|
|
|
|
if (redisClient.isHealthy()) {
|
|
|
|
|
try {
|
|
|
|
|
const luaResult = await redisClient.purchaseTicket(eventId, purchaseId, timestamp);
|
|
|
|
|
|
|
|
|
|
if (luaResult[0]) {
|
|
|
|
|
// Success - generate PDF ticket
|
|
|
|
|
try {
|
|
|
|
|
// Get event details for PDF
|
|
|
|
|
const eventStats = await redisClient.getEventStats(eventId);
|
|
|
|
|
|
|
|
|
|
const pdfResult = await pdfGenerator.generateTicketPDF({
|
|
|
|
|
ticketId: luaResult[0],
|
|
|
|
|
eventId,
|
|
|
|
|
purchaseId,
|
|
|
|
|
eventName: eventStats?.name || `Event ${eventId}`,
|
|
|
|
|
eventDescription: eventStats?.description || 'Event description not available',
|
|
|
|
|
timestamp,
|
|
|
|
|
soldCount: luaResult[2]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
result = {
|
|
|
|
|
success: true,
|
|
|
|
|
ticket: luaResult[0],
|
|
|
|
|
purchaseId,
|
|
|
|
|
eventId,
|
|
|
|
|
soldCount: luaResult[2],
|
|
|
|
|
message: 'Ticket purchased successfully!',
|
|
|
|
|
usingFallback: false,
|
|
|
|
|
pdf: {
|
|
|
|
|
generated: pdfResult.success,
|
|
|
|
|
filename: pdfResult.filename,
|
|
|
|
|
downloadUrl: `/tickets/${purchaseId}`
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
logger.logPurchase(eventId, luaResult[0], purchaseId, true);
|
|
|
|
|
logger.info(`PDF ticket generated for purchase ${purchaseId}`);
|
|
|
|
|
|
|
|
|
|
} catch (pdfError) {
|
|
|
|
|
logger.error('PDF generation failed:', pdfError);
|
|
|
|
|
|
|
|
|
|
// Still return success for ticket purchase, but note PDF failure
|
|
|
|
|
result = {
|
|
|
|
|
success: true,
|
|
|
|
|
ticket: luaResult[0],
|
|
|
|
|
purchaseId,
|
|
|
|
|
eventId,
|
|
|
|
|
soldCount: luaResult[2],
|
|
|
|
|
message: 'Ticket purchased successfully! (PDF generation failed)',
|
|
|
|
|
usingFallback: false,
|
|
|
|
|
pdf: {
|
|
|
|
|
generated: false,
|
|
|
|
|
error: 'PDF generation failed'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
logger.logPurchase(eventId, luaResult[0], purchaseId, true);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Failed - handle specific error
|
|
|
|
|
const errorCode = luaResult[1];
|
|
|
|
|
let statusCode = 400;
|
|
|
|
|
let message = 'Purchase failed';
|
|
|
|
|
|
|
|
|
|
switch (errorCode) {
|
|
|
|
|
case 'EVENT_NOT_FOUND':
|
|
|
|
|
statusCode = 404;
|
|
|
|
|
message = 'Event not found';
|
|
|
|
|
break;
|
|
|
|
|
case 'NO_TICKETS_AVAILABLE':
|
|
|
|
|
statusCode = 409;
|
|
|
|
|
message = 'No tickets available for this event';
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.logPurchase(eventId, null, purchaseId, false, errorCode);
|
|
|
|
|
return res.status(statusCode).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message,
|
|
|
|
|
errorCode,
|
|
|
|
|
eventId,
|
|
|
|
|
purchaseId
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (redisError) {
|
|
|
|
|
logger.error('Redis purchase failed, attempting fallback:', redisError);
|
|
|
|
|
// Activate fallback if not already active
|
|
|
|
|
if (!fallbackStore.isActive) {
|
|
|
|
|
fallbackStore.activate('Redis purchase operation failed');
|
|
|
|
|
}
|
|
|
|
|
throw redisError; // Will be caught by outer try-catch for fallback
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error('Redis not available');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const responseTime = Date.now() - startTime;
|
|
|
|
|
result.responseTime = `${responseTime}ms`;
|
|
|
|
|
|
|
|
|
|
res.json(result);
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Fallback to in-memory store
|
|
|
|
|
try {
|
|
|
|
|
if (!fallbackStore.isActive) {
|
|
|
|
|
fallbackStore.activate('Redis connection failed during purchase');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fallbackResult = fallbackStore.purchaseTicket(eventId, purchaseId);
|
|
|
|
|
|
|
|
|
|
if (fallbackResult.success) {
|
|
|
|
|
// Generate PDF for fallback purchase
|
|
|
|
|
try {
|
|
|
|
|
const eventStats = fallbackStore.getEventStats(eventId);
|
|
|
|
|
|
|
|
|
|
const pdfResult = await pdfGenerator.generateTicketPDF({
|
|
|
|
|
ticketId: fallbackResult.ticket,
|
|
|
|
|
eventId,
|
|
|
|
|
purchaseId,
|
|
|
|
|
eventName: eventStats?.name || `Event ${eventId}`,
|
|
|
|
|
eventDescription: eventStats?.description || 'Event description not available',
|
|
|
|
|
timestamp,
|
|
|
|
|
soldCount: fallbackResult.soldCount
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const responseTime = Date.now() - startTime;
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
ticket: fallbackResult.ticket,
|
|
|
|
|
purchaseId,
|
|
|
|
|
eventId,
|
|
|
|
|
soldCount: fallbackResult.soldCount,
|
|
|
|
|
message: 'Ticket purchased successfully (fallback mode)!',
|
|
|
|
|
usingFallback: true,
|
|
|
|
|
responseTime: `${responseTime}ms`,
|
|
|
|
|
pdf: {
|
|
|
|
|
generated: pdfResult.success,
|
|
|
|
|
filename: pdfResult.filename,
|
|
|
|
|
downloadUrl: `/tickets/${purchaseId}`
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
logger.info(`PDF ticket generated for fallback purchase ${purchaseId}`);
|
|
|
|
|
|
|
|
|
|
} catch (pdfError) {
|
|
|
|
|
logger.error('PDF generation failed in fallback mode:', pdfError);
|
|
|
|
|
|
|
|
|
|
const responseTime = Date.now() - startTime;
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
ticket: fallbackResult.ticket,
|
|
|
|
|
purchaseId,
|
|
|
|
|
eventId,
|
|
|
|
|
soldCount: fallbackResult.soldCount,
|
|
|
|
|
message: 'Ticket purchased successfully (fallback mode, PDF generation failed)!',
|
|
|
|
|
usingFallback: true,
|
|
|
|
|
responseTime: `${responseTime}ms`,
|
|
|
|
|
pdf: {
|
|
|
|
|
generated: false,
|
|
|
|
|
error: 'PDF generation failed'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let statusCode = 400;
|
|
|
|
|
let message = 'Purchase failed';
|
|
|
|
|
|
|
|
|
|
switch (fallbackResult.error) {
|
|
|
|
|
case 'EVENT_NOT_FOUND':
|
|
|
|
|
statusCode = 404;
|
|
|
|
|
message = 'Event not found';
|
|
|
|
|
break;
|
|
|
|
|
case 'NO_TICKETS_AVAILABLE':
|
|
|
|
|
statusCode = 409;
|
|
|
|
|
message = 'No tickets available for this event';
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.logPurchase(eventId, null, purchaseId, false, fallbackResult.error);
|
|
|
|
|
res.status(statusCode).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message,
|
|
|
|
|
errorCode: fallbackResult.error,
|
|
|
|
|
eventId,
|
|
|
|
|
purchaseId,
|
|
|
|
|
usingFallback: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (fallbackError) {
|
|
|
|
|
logger.error('Both Redis and fallback failed:', fallbackError);
|
|
|
|
|
logger.logPurchase(eventId, null, purchaseId, false, fallbackError);
|
|
|
|
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: 'System temporarily unavailable',
|
|
|
|
|
eventId,
|
|
|
|
|
purchaseId
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-02-11 14:52:32 +01:00
|
|
|
|
2025-07-29 21:48:34 +01:00
|
|
|
// Download ticket PDF endpoint
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
if (!pdfGenerator.ticketExists(purchaseId)) {
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: 'Ticket not found'
|
2025-02-11 14:52:32 +01:00
|
|
|
});
|
2025-07-29 21:48:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const filepath = pdfGenerator.getTicketPath(purchaseId);
|
|
|
|
|
const filename = `ticket-${purchaseId}.pdf`;
|
|
|
|
|
|
|
|
|
|
res.setHeader('Content-Type', 'application/pdf');
|
|
|
|
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
|
|
|
|
|
|
|
|
const fileStream = require('fs').createReadStream(filepath);
|
|
|
|
|
fileStream.pipe(res);
|
|
|
|
|
|
|
|
|
|
logger.info(`PDF ticket downloaded: ${purchaseId}`);
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Error downloading ticket:', error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: 'Failed to download ticket'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// PDF management endpoint
|
|
|
|
|
app.get('/admin/pdf-stats', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const stats = pdfGenerator.getStats();
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
stats
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Error getting PDF stats:', error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: 'Failed to get PDF statistics'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Cleanup old tickets endpoint
|
|
|
|
|
app.post('/admin/cleanup-tickets', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const maxAgeHours = req.body.maxAgeHours || 24;
|
|
|
|
|
const deletedCount = await pdfGenerator.cleanupOldTickets(maxAgeHours);
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
message: `Cleaned up ${deletedCount} old tickets`,
|
|
|
|
|
deletedCount
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Error cleaning up tickets:', error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: 'Failed to cleanup tickets'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Metrics endpoint (Prometheus compatible)
|
|
|
|
|
app.get('/metrics', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
let globalStats, events;
|
|
|
|
|
|
|
|
|
|
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-07-29 21:48:34 +01:00
|
|
|
|
|
|
|
|
// Get PDF stats
|
|
|
|
|
const pdfStats = pdfGenerator.getStats();
|
|
|
|
|
|
|
|
|
|
// Calculate metrics
|
|
|
|
|
const metrics = {
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
global: globalStats,
|
|
|
|
|
events: events,
|
|
|
|
|
system: {
|
|
|
|
|
usingFallback: fallbackStore.isActive,
|
|
|
|
|
redisConnected: redisClient.isHealthy(),
|
|
|
|
|
uptime: process.uptime(),
|
|
|
|
|
memoryUsage: process.memoryUsage()
|
|
|
|
|
},
|
|
|
|
|
pdf: pdfStats
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
res.json(metrics);
|
2025-02-11 14:52:32 +01:00
|
|
|
} catch (error) {
|
2025-07-29 21:48:34 +01:00
|
|
|
logger.error('Error generating metrics:', error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
|
|
|
|
message: 'Failed to generate metrics'
|
|
|
|
|
});
|
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();
|
|
|
|
|
logger.info('Redis connected successfully');
|
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
logger.error('Failed to initialize server:', error);
|
|
|
|
|
logger.warn('Starting server with fallback store only');
|
|
|
|
|
|
|
|
|
|
fallbackStore.activate('Redis connection failed at startup');
|
|
|
|
|
|
|
|
|
|
app.listen(port, () => {
|
|
|
|
|
logger.warn(`⚠️ Server running in FALLBACK MODE on port ${port}`);
|
|
|
|
|
logger.warn('Redis connection failed - using in-memory store');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Graceful shutdown
|
|
|
|
|
process.on('SIGINT', async () => {
|
|
|
|
|
logger.info('Received SIGINT, shutting down gracefully...');
|
|
|
|
|
try {
|
|
|
|
|
await redisClient.disconnect();
|
|
|
|
|
logger.info('Redis disconnected');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Error disconnecting Redis:', error);
|
|
|
|
|
}
|
|
|
|
|
process.exit(0);
|
2025-02-11 14:52:32 +01:00
|
|
|
});
|
2025-07-29 21:48:34 +01:00
|
|
|
|
|
|
|
|
process.on('SIGTERM', async () => {
|
|
|
|
|
logger.info('Received SIGTERM, shutting down gracefully...');
|
|
|
|
|
try {
|
|
|
|
|
await redisClient.disconnect();
|
|
|
|
|
logger.info('Redis disconnected');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Error disconnecting Redis:', error);
|
|
|
|
|
}
|
|
|
|
|
process.exit(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Start the server
|
|
|
|
|
initializeServer();
|