feat: add autocannon and fix purchase ticket flow

This commit is contained in:
Ayobami
2025-07-30 22:31:34 +01:00
parent 42fec5708a
commit 064ae104f7
6 changed files with 173 additions and 103 deletions
-21
View File
@@ -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
+2
View File
@@ -1,3 +1,5 @@
/node_modules /node_modules
/dist /dist
/package-lock.json /package-lock.json
/tickets
.env
+58
View File
@@ -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();
+4 -3
View File
@@ -32,13 +32,14 @@ redis.call('HINCRBY', globalKey, 'totalSold', 1)
-- Store purchase record -- Store purchase record
local purchaseKey = 'purchase:' .. purchaseId local purchaseKey = 'purchase:' .. purchaseId
redis.call('HSET', purchaseKey, { local eventIdFromKey = string.match(ticketKey, 'event:(%d+):tickets')
redis.call('HSET', purchaseKey,
'ticketId', ticket, 'ticketId', ticket,
'eventId', string.match(ticketKey, 'event:(%d+):tickets'), 'eventId', eventIdFromKey,
'purchaseId', purchaseId, 'purchaseId', purchaseId,
'timestamp', timestamp, 'timestamp', timestamp,
'status', 'completed' 'status', 'completed'
}) )
-- Set expiration for purchase record (24 hours) -- Set expiration for purchase record (24 hours)
redis.call('EXPIRE', purchaseKey, 86400) redis.call('EXPIRE', purchaseKey, 86400)
+82 -62
View File
@@ -1,11 +1,11 @@
const PDFDocument = require('pdfkit'); const PDFDocument = require("pdfkit");
const fs = require('fs'); const fs = require("fs");
const path = require('path'); const path = require("path");
const logger = require('./logger'); const logger = require("./logger");
class PDFGenerator { class PDFGenerator {
constructor() { constructor() {
this.outputDir = process.env.PDF_OUTPUT_DIR || './tickets'; this.outputDir = process.env.PDF_OUTPUT_DIR || "./tickets";
this.ensureOutputDirectory(); this.ensureOutputDirectory();
} }
@@ -26,13 +26,13 @@ class PDFGenerator {
eventName, eventName,
eventDescription, eventDescription,
timestamp, timestamp,
soldCount soldCount,
} = ticketData; } = ticketData;
// Create PDF document // Create PDF document
const doc = new PDFDocument({ const doc = new PDFDocument({
size: 'A4', size: "A4",
margin: 50 margin: 50,
}); });
// Generate filename // Generate filename
@@ -44,97 +44,114 @@ class PDFGenerator {
doc.pipe(stream); doc.pipe(stream);
// Header // Header
doc.fontSize(24) doc
.fillColor('#2c3e50') .fontSize(24)
.text('🎫 TICKET RECEIPT', 50, 50, { align: 'center' }); .fillColor("#2c3e50")
.text("TICKET RECEIPT", 50, 50, { align: "center" });
// Divider line // Divider line
doc.moveTo(50, 90) doc
.moveTo(50, 90)
.lineTo(545, 90) .lineTo(545, 90)
.strokeColor('#3498db') .strokeColor("#3498db")
.lineWidth(2) .lineWidth(2)
.stroke(); .stroke();
// Event Information // Event Information
doc.fontSize(18) doc
.fillColor('#2c3e50') .fontSize(18)
.text('Event Information', 50, 120); .fillColor("#2c3e50")
.text("Event Information", 50, 120);
doc.fontSize(12) doc
.fillColor('#34495e') .fontSize(12)
.fillColor("#34495e")
.text(`Event Name: ${eventName || `Event ${eventId}`}`, 50, 150) .text(`Event Name: ${eventName || `Event ${eventId}`}`, 50, 150)
.text(`Event ID: ${eventId}`, 50, 170) .text(`Event ID: ${eventId}`, 50, 170)
.text(`Description: ${eventDescription || 'No description available'}`, 50, 190); .text(
`Description: ${eventDescription || "No description available"}`,
50,
190
);
// Ticket Information // Ticket Information
doc.fontSize(18) doc.fontSize(18).fillColor("#2c3e50").text("Ticket Details", 50, 230);
.fillColor('#2c3e50')
.text('Ticket Details', 50, 230);
doc.fontSize(12) doc
.fillColor('#34495e') .fontSize(12)
.fillColor("#34495e")
.text(`Ticket ID: ${ticketId}`, 50, 260) .text(`Ticket ID: ${ticketId}`, 50, 260)
.text(`Purchase ID: ${purchaseId}`, 50, 280) .text(`Purchase ID: ${purchaseId}`, 50, 280)
.text(`Purchase Date: ${new Date(timestamp).toLocaleString()}`, 50, 300) .text(
`Purchase Date: ${new Date(timestamp).toLocaleString()}`,
50,
300
)
.text(`Ticket Number: #${soldCount}`, 50, 320); .text(`Ticket Number: #${soldCount}`, 50, 320);
// QR Code placeholder (you could integrate a QR code library here) // QR Code placeholder (you could integrate a QR code library here)
doc.rect(400, 250, 100, 100) doc
.strokeColor('#bdc3c7') .rect(400, 250, 100, 100)
.strokeColor("#bdc3c7")
.lineWidth(1) .lineWidth(1)
.stroke(); .stroke();
doc.fontSize(10) doc
.fillColor('#7f8c8d') .fontSize(10)
.text('QR Code', 430, 305, { align: 'center' }); .fillColor("#7f8c8d")
.text("QR Code", 430, 305, { align: "center" });
// Terms and Conditions // Terms and Conditions
doc.fontSize(14) doc
.fillColor('#2c3e50') .fontSize(14)
.text('Terms & Conditions', 50, 380); .fillColor("#2c3e50")
.text("Terms & Conditions", 50, 380);
doc.fontSize(10) doc
.fillColor('#7f8c8d') .fontSize(10)
.text('• This ticket is non-refundable and non-transferable', 50, 410) .fillColor("#7f8c8d")
.text('• Please arrive 30 minutes before the event starts', 50, 425) .text("• This ticket is non-refundable and non-transferable", 50, 410)
.text('• Valid photo ID required for entry', 50, 440) .text("• Please arrive 30 minutes before the event starts", 50, 425)
.text('• This ticket is valid only for the specified event and date', 50, 455); .text("• Valid photo ID required for entry", 50, 440)
.text(
"• This ticket is valid only for the specified event and date",
50,
455
);
// Footer // Footer
doc.fontSize(8) doc
.fillColor('#95a5a6') .fontSize(8)
.fillColor("#95a5a6")
.text(`Generated on ${new Date().toLocaleString()}`, 50, 520) .text(`Generated on ${new Date().toLocaleString()}`, 50, 520)
.text('Powered by Ticket Microservice', 50, 535) .text("Powered by Ticket Microservice", 50, 535)
.text(`System ID: ${process.env.NODE_ENV || 'development'}`, 50, 550); .text(`System ID: ${process.env.NODE_ENV || "development"}`, 50, 550);
// Security watermark // Security watermark
doc.fontSize(60) doc.fontSize(60).fillColor("#ecf0f1").text("VALID", 200, 300, {
.fillColor('#ecf0f1')
.text('VALID', 200, 300, {
rotate: -45, rotate: -45,
opacity: 0.1 opacity: 0.1,
}); });
// Finalize PDF // Finalize PDF
doc.end(); doc.end();
stream.on('finish', () => { stream.on("finish", () => {
logger.info(`PDF ticket generated: ${filename}`); logger.info(`PDF ticket generated: ${filename}`);
resolve({ resolve({
success: true, success: true,
filename, filename,
filepath, filepath,
size: fs.statSync(filepath).size size: fs.statSync(filepath).size,
}); });
}); });
stream.on('error', (error) => { stream.on("error", (error) => {
logger.error('Error writing PDF file:', error); logger.error("Error writing PDF file:", error);
reject(error); reject(error);
}); });
} catch (error) { } catch (error) {
logger.error('Error generating PDF:', error); logger.error("Error generating PDF:", error);
reject(error); reject(error);
} }
}); });
@@ -148,11 +165,14 @@ class PDFGenerator {
const result = await this.generateTicketPDF(ticket); const result = await this.generateTicketPDF(ticket);
results.push(result); results.push(result);
} catch (error) { } 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({ results.push({
success: false, success: false,
ticketId: ticket.ticketId, ticketId: ticket.ticketId,
error: error.message error: error.message,
}); });
} }
} }
@@ -191,7 +211,7 @@ class PDFGenerator {
let deletedCount = 0; let deletedCount = 0;
for (const file of files) { for (const file of files) {
if (file.endsWith('.pdf')) { if (file.endsWith(".pdf")) {
const filepath = path.join(this.outputDir, file); const filepath = path.join(this.outputDir, file);
const stats = fs.statSync(filepath); const stats = fs.statSync(filepath);
const ageHours = (now - stats.mtime.getTime()) / (1000 * 60 * 60); const ageHours = (now - stats.mtime.getTime()) / (1000 * 60 * 60);
@@ -209,7 +229,7 @@ class PDFGenerator {
return deletedCount; return deletedCount;
} catch (error) { } catch (error) {
logger.error('Error cleaning up old tickets:', error); logger.error("Error cleaning up old tickets:", error);
throw error; throw error;
} }
} }
@@ -217,7 +237,7 @@ class PDFGenerator {
getStats() { getStats() {
try { try {
const files = fs.readdirSync(this.outputDir); 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; let totalSize = 0;
for (const file of pdfFiles) { for (const file of pdfFiles) {
@@ -229,16 +249,16 @@ class PDFGenerator {
totalTickets: pdfFiles.length, totalTickets: pdfFiles.length,
totalSizeBytes: totalSize, totalSizeBytes: totalSize,
totalSizeMB: (totalSize / (1024 * 1024)).toFixed(2), totalSizeMB: (totalSize / (1024 * 1024)).toFixed(2),
outputDirectory: this.outputDir outputDirectory: this.outputDir,
}; };
} catch (error) { } catch (error) {
logger.error('Error getting PDF stats:', error); logger.error("Error getting PDF stats:", error);
return { return {
totalTickets: 0, totalTickets: 0,
totalSizeBytes: 0, totalSizeBytes: 0,
totalSizeMB: '0.00', totalSizeMB: "0.00",
outputDirectory: this.outputDir, outputDirectory: this.outputDir,
error: error.message error: error.message,
}; };
} }
} }
+11 -1
View File
@@ -70,12 +70,20 @@ class RedisClient {
throw new Error('Redis not connected'); 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 = [ const keys = [
`event:${eventId}:tickets`, `event:${eventId}:tickets`,
`event:${eventId}:meta`, `event:${eventId}:meta`,
'global:stats' 'global:stats'
]; ];
const args = [timestamp, purchaseId]; // Ensure all arguments are strings as required by Redis Lua
const args = [String(timestamp), String(purchaseId)];
try { try {
const result = await this.client.eval( const result = await this.client.eval(
@@ -85,6 +93,8 @@ class RedisClient {
return result; return result;
} catch (error) { } catch (error) {
logger.error('Error executing purchase ticket script:', error); logger.error('Error executing purchase ticket script:', error);
logger.error('Script keys:', keys);
logger.error('Script args:', args);
throw error; throw error;
} }
} }