feat: complete phase one and two of task implementation
This commit is contained in:
@@ -1,35 +1,486 @@
|
||||
const express = require("express");
|
||||
const redis = require("redis");
|
||||
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'));
|
||||
|
||||
// Create Redis client and connect
|
||||
const client = redis.createClient();
|
||||
client.connect().catch(console.error);
|
||||
// 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();
|
||||
});
|
||||
|
||||
// Purchase ticket endpoint
|
||||
app.post("/buy", async (req, res) => {
|
||||
// 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 {
|
||||
// Atomically pop a ticket from the 'tickets' list
|
||||
const ticket = await client.lPop("tickets");
|
||||
if (ticket) {
|
||||
res.json({
|
||||
success: true,
|
||||
ticket,
|
||||
message: "Ticket purchased successfully!",
|
||||
});
|
||||
let events;
|
||||
|
||||
if (redisClient.isHealthy()) {
|
||||
events = await redisClient.getAllEvents();
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: "No tickets available" });
|
||||
events = fallbackStore.getAllEvents();
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
events,
|
||||
usingFallback: fallbackStore.isActive
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error during ticket purchase:", error);
|
||||
res.status(500).json({ success: false, message: "Internal Server Error" });
|
||||
logger.error('Error fetching events:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch events'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running on port ${port}`);
|
||||
// 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();
|
||||
|
||||
Reference in New Issue
Block a user