From 064ae104f70f1a161a57c74a4e2ae3c39cd0adff Mon Sep 17 00:00:00 2001 From: Ayobami Date: Wed, 30 Jul 2025 22:31:34 +0100 Subject: [PATCH] feat: add autocannon and fix purchase ticket flow --- .env.example | 21 ----- .gitignore | 4 +- debug-events.js | 58 ++++++++++++ src/lua/purchase-ticket.lua | 7 +- src/utils/pdf-generator.js | 174 ++++++++++++++++++++---------------- src/utils/redis-client.js | 12 ++- 6 files changed, 173 insertions(+), 103 deletions(-) delete mode 100644 .env.example create mode 100644 debug-events.js diff --git a/.env.example b/.env.example deleted file mode 100644 index d1af061..0000000 --- a/.env.example +++ /dev/null @@ -1,21 +0,0 @@ -# Server Configuration -PORT=3049 -NODE_ENV=development - -# Redis Configuration -REDIS_URL=redis://localhost:6379 -REDIS_HOST=localhost -REDIS_PORT=6379 - -# Event Configuration -DEFAULT_TICKETS_PER_EVENT=10000 - -# Logging Configuration -LOG_LEVEL=info -LOG_FILE=logs/app.log - -# PDF Configuration -PDF_OUTPUT_DIR=./tickets - -# Metrics Configuration -METRICS_PORT=9090 diff --git a/.gitignore b/.gitignore index bd1e393..33e55ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /node_modules /dist -/package-lock.json \ No newline at end of file +/package-lock.json +/tickets +.env \ No newline at end of file diff --git a/debug-events.js b/debug-events.js new file mode 100644 index 0000000..b259d58 --- /dev/null +++ b/debug-events.js @@ -0,0 +1,58 @@ +const redis = require('redis'); +require('dotenv').config(); + +async function debugEvents() { + const client = redis.createClient({ + url: process.env.REDIS_URL || 'redis://localhost:6379' + }); + + try { + await client.connect(); + console.log('āœ… Connected to Redis'); + + // Check what event keys exist + const eventKeys = await client.keys('event:*'); + console.log('\nšŸ“‹ Found Redis keys:', eventKeys); + + // Check global stats + const globalStats = await client.hGetAll('global:stats'); + console.log('\nšŸŒ Global stats:', globalStats); + + // Check each event + const metaKeys = eventKeys.filter(key => key.includes(':meta')); + console.log('\nšŸŽ« Event Details:'); + + for (const metaKey of metaKeys) { + const eventId = metaKey.match(/event:(\d+):meta/)[1]; + const ticketKey = `event:${eventId}:tickets`; + + const meta = await client.hGetAll(metaKey); + const ticketCount = await client.lLen(ticketKey); + + console.log(`\n Event ${eventId}:`); + console.log(` Name: ${meta.name}`); + console.log(` Total Tickets: ${meta.totalTickets}`); + console.log(` Sold Tickets: ${meta.soldTickets}`); + console.log(` Remaining: ${ticketCount}`); + console.log(` Created: ${meta.createdAt}`); + } + + // Test if we can check existence of event 5 + const event5Exists = await client.exists('event:5:meta'); + console.log(`\nšŸ” Event 5 exists: ${event5Exists ? 'YES' : 'NO'}`); + + if (event5Exists) { + const event5Meta = await client.hGetAll('event:5:meta'); + const event5Tickets = await client.lLen('event:5:tickets'); + console.log('šŸ“Š Event 5 details:', event5Meta); + console.log(`šŸŽ« Event 5 remaining tickets: ${event5Tickets}`); + } + + } catch (error) { + console.error('āŒ Error:', error); + } finally { + await client.disconnect(); + } +} + +debugEvents(); diff --git a/src/lua/purchase-ticket.lua b/src/lua/purchase-ticket.lua index c5dcde8..5437a80 100644 --- a/src/lua/purchase-ticket.lua +++ b/src/lua/purchase-ticket.lua @@ -32,13 +32,14 @@ redis.call('HINCRBY', globalKey, 'totalSold', 1) -- Store purchase record local purchaseKey = 'purchase:' .. purchaseId -redis.call('HSET', purchaseKey, { +local eventIdFromKey = string.match(ticketKey, 'event:(%d+):tickets') +redis.call('HSET', purchaseKey, 'ticketId', ticket, - 'eventId', string.match(ticketKey, 'event:(%d+):tickets'), + 'eventId', eventIdFromKey, 'purchaseId', purchaseId, 'timestamp', timestamp, 'status', 'completed' -}) +) -- Set expiration for purchase record (24 hours) redis.call('EXPIRE', purchaseKey, 86400) diff --git a/src/utils/pdf-generator.js b/src/utils/pdf-generator.js index 3f5f894..9ef5e14 100644 --- a/src/utils/pdf-generator.js +++ b/src/utils/pdf-generator.js @@ -1,11 +1,11 @@ -const PDFDocument = require('pdfkit'); -const fs = require('fs'); -const path = require('path'); -const logger = require('./logger'); +const PDFDocument = require("pdfkit"); +const fs = require("fs"); +const path = require("path"); +const logger = require("./logger"); class PDFGenerator { constructor() { - this.outputDir = process.env.PDF_OUTPUT_DIR || './tickets'; + this.outputDir = process.env.PDF_OUTPUT_DIR || "./tickets"; this.ensureOutputDirectory(); } @@ -26,13 +26,13 @@ class PDFGenerator { eventName, eventDescription, timestamp, - soldCount + soldCount, } = ticketData; // Create PDF document const doc = new PDFDocument({ - size: 'A4', - margin: 50 + size: "A4", + margin: 50, }); // Generate filename @@ -44,97 +44,114 @@ class PDFGenerator { doc.pipe(stream); // Header - doc.fontSize(24) - .fillColor('#2c3e50') - .text('šŸŽ« TICKET RECEIPT', 50, 50, { align: 'center' }); + doc + .fontSize(24) + .fillColor("#2c3e50") + .text("TICKET RECEIPT", 50, 50, { align: "center" }); // Divider line - doc.moveTo(50, 90) - .lineTo(545, 90) - .strokeColor('#3498db') - .lineWidth(2) - .stroke(); + doc + .moveTo(50, 90) + .lineTo(545, 90) + .strokeColor("#3498db") + .lineWidth(2) + .stroke(); // Event Information - doc.fontSize(18) - .fillColor('#2c3e50') - .text('Event Information', 50, 120); + doc + .fontSize(18) + .fillColor("#2c3e50") + .text("Event Information", 50, 120); - doc.fontSize(12) - .fillColor('#34495e') - .text(`Event Name: ${eventName || `Event ${eventId}`}`, 50, 150) - .text(`Event ID: ${eventId}`, 50, 170) - .text(`Description: ${eventDescription || 'No description available'}`, 50, 190); + doc + .fontSize(12) + .fillColor("#34495e") + .text(`Event Name: ${eventName || `Event ${eventId}`}`, 50, 150) + .text(`Event ID: ${eventId}`, 50, 170) + .text( + `Description: ${eventDescription || "No description available"}`, + 50, + 190 + ); // Ticket Information - doc.fontSize(18) - .fillColor('#2c3e50') - .text('Ticket Details', 50, 230); + doc.fontSize(18).fillColor("#2c3e50").text("Ticket Details", 50, 230); - doc.fontSize(12) - .fillColor('#34495e') - .text(`Ticket ID: ${ticketId}`, 50, 260) - .text(`Purchase ID: ${purchaseId}`, 50, 280) - .text(`Purchase Date: ${new Date(timestamp).toLocaleString()}`, 50, 300) - .text(`Ticket Number: #${soldCount}`, 50, 320); + doc + .fontSize(12) + .fillColor("#34495e") + .text(`Ticket ID: ${ticketId}`, 50, 260) + .text(`Purchase ID: ${purchaseId}`, 50, 280) + .text( + `Purchase Date: ${new Date(timestamp).toLocaleString()}`, + 50, + 300 + ) + .text(`Ticket Number: #${soldCount}`, 50, 320); // QR Code placeholder (you could integrate a QR code library here) - doc.rect(400, 250, 100, 100) - .strokeColor('#bdc3c7') - .lineWidth(1) - .stroke(); + doc + .rect(400, 250, 100, 100) + .strokeColor("#bdc3c7") + .lineWidth(1) + .stroke(); - doc.fontSize(10) - .fillColor('#7f8c8d') - .text('QR Code', 430, 305, { align: 'center' }); + doc + .fontSize(10) + .fillColor("#7f8c8d") + .text("QR Code", 430, 305, { align: "center" }); // Terms and Conditions - doc.fontSize(14) - .fillColor('#2c3e50') - .text('Terms & Conditions', 50, 380); + doc + .fontSize(14) + .fillColor("#2c3e50") + .text("Terms & Conditions", 50, 380); - doc.fontSize(10) - .fillColor('#7f8c8d') - .text('• This ticket is non-refundable and non-transferable', 50, 410) - .text('• Please arrive 30 minutes before the event starts', 50, 425) - .text('• Valid photo ID required for entry', 50, 440) - .text('• This ticket is valid only for the specified event and date', 50, 455); + doc + .fontSize(10) + .fillColor("#7f8c8d") + .text("• This ticket is non-refundable and non-transferable", 50, 410) + .text("• Please arrive 30 minutes before the event starts", 50, 425) + .text("• Valid photo ID required for entry", 50, 440) + .text( + "• This ticket is valid only for the specified event and date", + 50, + 455 + ); // Footer - doc.fontSize(8) - .fillColor('#95a5a6') - .text(`Generated on ${new Date().toLocaleString()}`, 50, 520) - .text('Powered by Ticket Microservice', 50, 535) - .text(`System ID: ${process.env.NODE_ENV || 'development'}`, 50, 550); + doc + .fontSize(8) + .fillColor("#95a5a6") + .text(`Generated on ${new Date().toLocaleString()}`, 50, 520) + .text("Powered by Ticket Microservice", 50, 535) + .text(`System ID: ${process.env.NODE_ENV || "development"}`, 50, 550); // Security watermark - doc.fontSize(60) - .fillColor('#ecf0f1') - .text('VALID', 200, 300, { - rotate: -45, - opacity: 0.1 - }); + doc.fontSize(60).fillColor("#ecf0f1").text("VALID", 200, 300, { + rotate: -45, + opacity: 0.1, + }); // Finalize PDF doc.end(); - stream.on('finish', () => { + stream.on("finish", () => { logger.info(`PDF ticket generated: ${filename}`); resolve({ success: true, filename, filepath, - size: fs.statSync(filepath).size + size: fs.statSync(filepath).size, }); }); - stream.on('error', (error) => { - logger.error('Error writing PDF file:', error); + stream.on("error", (error) => { + logger.error("Error writing PDF file:", error); reject(error); }); - } catch (error) { - logger.error('Error generating PDF:', error); + logger.error("Error generating PDF:", error); reject(error); } }); @@ -142,17 +159,20 @@ class PDFGenerator { async generateBulkTicketsPDF(tickets) { const results = []; - + for (const ticket of tickets) { try { const result = await this.generateTicketPDF(ticket); results.push(result); } catch (error) { - logger.error(`Failed to generate PDF for ticket ${ticket.ticketId}:`, error); + logger.error( + `Failed to generate PDF for ticket ${ticket.ticketId}:`, + error + ); results.push({ success: false, ticketId: ticket.ticketId, - error: error.message + error: error.message, }); } } @@ -191,7 +211,7 @@ class PDFGenerator { let deletedCount = 0; for (const file of files) { - if (file.endsWith('.pdf')) { + if (file.endsWith(".pdf")) { const filepath = path.join(this.outputDir, file); const stats = fs.statSync(filepath); const ageHours = (now - stats.mtime.getTime()) / (1000 * 60 * 60); @@ -209,7 +229,7 @@ class PDFGenerator { return deletedCount; } catch (error) { - logger.error('Error cleaning up old tickets:', error); + logger.error("Error cleaning up old tickets:", error); throw error; } } @@ -217,8 +237,8 @@ class PDFGenerator { getStats() { try { const files = fs.readdirSync(this.outputDir); - const pdfFiles = files.filter(f => f.endsWith('.pdf')); - + const pdfFiles = files.filter((f) => f.endsWith(".pdf")); + let totalSize = 0; for (const file of pdfFiles) { const filepath = path.join(this.outputDir, file); @@ -229,16 +249,16 @@ class PDFGenerator { totalTickets: pdfFiles.length, totalSizeBytes: totalSize, totalSizeMB: (totalSize / (1024 * 1024)).toFixed(2), - outputDirectory: this.outputDir + outputDirectory: this.outputDir, }; } catch (error) { - logger.error('Error getting PDF stats:', error); + logger.error("Error getting PDF stats:", error); return { totalTickets: 0, totalSizeBytes: 0, - totalSizeMB: '0.00', + totalSizeMB: "0.00", outputDirectory: this.outputDir, - error: error.message + error: error.message, }; } } diff --git a/src/utils/redis-client.js b/src/utils/redis-client.js index bcb6830..1a4af98 100644 --- a/src/utils/redis-client.js +++ b/src/utils/redis-client.js @@ -70,12 +70,20 @@ class RedisClient { throw new Error('Redis not connected'); } + // Validate event exists before attempting purchase + const eventExists = await this.client.exists(`event:${eventId}:meta`); + if (!eventExists) { + logger.warn(`Event ${eventId} does not exist`); + return [null, 'EVENT_NOT_FOUND']; + } + const keys = [ `event:${eventId}:tickets`, `event:${eventId}:meta`, 'global:stats' ]; - const args = [timestamp, purchaseId]; + // Ensure all arguments are strings as required by Redis Lua + const args = [String(timestamp), String(purchaseId)]; try { const result = await this.client.eval( @@ -85,6 +93,8 @@ class RedisClient { return result; } catch (error) { logger.error('Error executing purchase ticket script:', error); + logger.error('Script keys:', keys); + logger.error('Script args:', args); throw error; } }