feat: add autocannon and fix purchase ticket flow
This commit is contained in:
@@ -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)
|
||||
|
||||
+97
-77
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user