feat: add autocannon and fix purchase ticket flow
This commit is contained in:
@@ -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
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/dist
|
/dist
|
||||||
/package-lock.json
|
/package-lock.json
|
||||||
|
/tickets
|
||||||
|
.env
|
||||||
@@ -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();
|
||||||
@@ -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
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user