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
/dist
/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
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)
+82 -62
View File
@@ -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)
doc
.moveTo(50, 90)
.lineTo(545, 90)
.strokeColor('#3498db')
.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')
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);
.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')
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(
`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')
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')
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);
.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, {
doc.fontSize(60).fillColor("#ecf0f1").text("VALID", 200, 300, {
rotate: -45,
opacity: 0.1
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);
}
});
@@ -148,11 +165,14 @@ class PDFGenerator {
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,7 +237,7 @@ 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) {
@@ -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,
};
}
}
+11 -1
View File
@@ -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;
}
}