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'); const app = express(); const port = process.env.PORT || 3049; // Middleware app.use(express.json()); 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' }); } }); // 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 }); } } }); // Download ticket PDF endpoint app.get('/tickets/:purchaseId', async (req, res) => { try { const purchaseId = req.params.purchaseId; if (!pdfGenerator.ticketExists(purchaseId)) { return res.status(404).json({ success: false, message: 'Ticket not found' }); } 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(); } else { globalStats = fallbackStore.getGlobalStats(); events = fallbackStore.getAllEvents(); } // 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); } catch (error) { logger.error('Error generating metrics:', error); res.status(500).json({ success: false, message: 'Failed to generate metrics' }); } }); // 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); }); 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();