feat: complete phase one and two of task implementation
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
-- Get event statistics script
|
||||
-- KEYS[1]: event metadata key (e.g., "event:1:meta")
|
||||
-- KEYS[2]: event tickets key (e.g., "event:1:tickets")
|
||||
|
||||
local metaKey = KEYS[1]
|
||||
local ticketKey = KEYS[2]
|
||||
|
||||
-- Check if event exists
|
||||
if redis.call('EXISTS', metaKey) == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Get event metadata
|
||||
local eventData = redis.call('HGETALL', metaKey)
|
||||
local eventInfo = {}
|
||||
for i = 1, #eventData, 2 do
|
||||
eventInfo[eventData[i]] = eventData[i + 1]
|
||||
end
|
||||
|
||||
-- Get remaining tickets count
|
||||
local remainingTickets = redis.call('LLEN', ticketKey)
|
||||
|
||||
-- Return combined stats
|
||||
return {
|
||||
eventInfo.eventId,
|
||||
eventInfo.name,
|
||||
eventInfo.description,
|
||||
eventInfo.totalTickets,
|
||||
eventInfo.soldTickets,
|
||||
remainingTickets,
|
||||
eventInfo.createdAt,
|
||||
eventInfo.lastSoldAt or "never"
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
-- Atomic ticket purchase script
|
||||
-- KEYS[1]: event tickets key (e.g., "event:1:tickets")
|
||||
-- KEYS[2]: event metadata key (e.g., "event:1:meta")
|
||||
-- KEYS[3]: global stats key ("global:stats")
|
||||
-- ARGV[1]: timestamp
|
||||
-- ARGV[2]: purchase ID (UUID)
|
||||
|
||||
local ticketKey = KEYS[1]
|
||||
local metaKey = KEYS[2]
|
||||
local globalKey = KEYS[3]
|
||||
local timestamp = ARGV[1]
|
||||
local purchaseId = ARGV[2]
|
||||
|
||||
-- Check if event exists
|
||||
if redis.call('EXISTS', metaKey) == 0 then
|
||||
return {nil, "EVENT_NOT_FOUND"}
|
||||
end
|
||||
|
||||
-- Atomically pop a ticket from the list
|
||||
local ticket = redis.call('LPOP', ticketKey)
|
||||
|
||||
if not ticket then
|
||||
return {nil, "NO_TICKETS_AVAILABLE"}
|
||||
end
|
||||
|
||||
-- Update event metadata
|
||||
local soldCount = redis.call('HINCRBY', metaKey, 'soldTickets', 1)
|
||||
redis.call('HSET', metaKey, 'lastSoldAt', timestamp)
|
||||
|
||||
-- Update global stats
|
||||
redis.call('HINCRBY', globalKey, 'totalSold', 1)
|
||||
|
||||
-- Store purchase record
|
||||
local purchaseKey = 'purchase:' .. purchaseId
|
||||
redis.call('HSET', purchaseKey, {
|
||||
'ticketId', ticket,
|
||||
'eventId', string.match(ticketKey, 'event:(%d+):tickets'),
|
||||
'purchaseId', purchaseId,
|
||||
'timestamp', timestamp,
|
||||
'status', 'completed'
|
||||
})
|
||||
|
||||
-- Set expiration for purchase record (24 hours)
|
||||
redis.call('EXPIRE', purchaseKey, 86400)
|
||||
|
||||
return {ticket, "SUCCESS", soldCount}
|
||||
@@ -0,0 +1,131 @@
|
||||
const logger = require('./logger');
|
||||
|
||||
class FallbackStore {
|
||||
constructor() {
|
||||
this.events = new Map();
|
||||
this.globalStats = {
|
||||
totalEvents: 0,
|
||||
totalTickets: 0,
|
||||
totalSold: 0,
|
||||
lastSeeded: null
|
||||
};
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
activate(reason) {
|
||||
this.isActive = true;
|
||||
logger.logFallback('In-Memory Store Activated', reason);
|
||||
logger.warn('⚠️ FALLBACK MODE: Using in-memory store - data will not persist!');
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
this.isActive = false;
|
||||
logger.info('In-Memory Store Deactivated - Redis connection restored');
|
||||
}
|
||||
|
||||
seedEvent(eventId, tickets, metadata) {
|
||||
if (!this.isActive) return false;
|
||||
|
||||
this.events.set(eventId, {
|
||||
tickets: [...tickets], // Create a copy
|
||||
metadata: { ...metadata },
|
||||
soldTickets: 0
|
||||
});
|
||||
|
||||
this.globalStats.totalEvents++;
|
||||
this.globalStats.totalTickets += tickets.length;
|
||||
|
||||
logger.info(`Fallback: Seeded event ${eventId} with ${tickets.length} tickets`);
|
||||
return true;
|
||||
}
|
||||
|
||||
purchaseTicket(eventId, purchaseId) {
|
||||
if (!this.isActive) return null;
|
||||
|
||||
const event = this.events.get(eventId);
|
||||
if (!event) {
|
||||
return { success: false, error: 'EVENT_NOT_FOUND' };
|
||||
}
|
||||
|
||||
if (event.tickets.length === 0) {
|
||||
return { success: false, error: 'NO_TICKETS_AVAILABLE' };
|
||||
}
|
||||
|
||||
// Atomically remove a ticket
|
||||
const ticket = event.tickets.shift();
|
||||
event.soldTickets++;
|
||||
event.metadata.lastSoldAt = new Date().toISOString();
|
||||
|
||||
this.globalStats.totalSold++;
|
||||
|
||||
logger.logPurchase(eventId, ticket, purchaseId, true);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
ticket,
|
||||
soldCount: event.soldTickets
|
||||
};
|
||||
}
|
||||
|
||||
getEventStats(eventId) {
|
||||
if (!this.isActive) return null;
|
||||
|
||||
const event = this.events.get(eventId);
|
||||
if (!event) return null;
|
||||
|
||||
return {
|
||||
eventId: eventId,
|
||||
name: event.metadata.name,
|
||||
description: event.metadata.description,
|
||||
totalTickets: parseInt(event.metadata.totalTickets),
|
||||
soldTickets: event.soldTickets,
|
||||
remainingTickets: event.tickets.length,
|
||||
createdAt: event.metadata.createdAt,
|
||||
lastSoldAt: event.metadata.lastSoldAt || 'never'
|
||||
};
|
||||
}
|
||||
|
||||
getAllEvents() {
|
||||
if (!this.isActive) return [];
|
||||
|
||||
const events = [];
|
||||
for (const [eventId, _] of this.events) {
|
||||
const stats = this.getEventStats(eventId);
|
||||
if (stats) {
|
||||
events.push(stats);
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
getGlobalStats() {
|
||||
if (!this.isActive) return null;
|
||||
return { ...this.globalStats };
|
||||
}
|
||||
|
||||
isHealthy() {
|
||||
return this.isActive;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.events.clear();
|
||||
this.globalStats = {
|
||||
totalEvents: 0,
|
||||
totalTickets: 0,
|
||||
totalSold: 0,
|
||||
lastSeeded: null
|
||||
};
|
||||
logger.info('Fallback store cleared');
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
active: this.isActive,
|
||||
eventsCount: this.events.size,
|
||||
totalTickets: this.globalStats.totalTickets,
|
||||
totalSold: this.globalStats.totalSold
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new FallbackStore();
|
||||
@@ -0,0 +1,105 @@
|
||||
const winston = require('winston');
|
||||
const path = require('path');
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
const fs = require('fs');
|
||||
const logsDir = path.join(process.cwd(), 'logs');
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Custom format for console output
|
||||
const consoleFormat = winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
||||
let metaStr = Object.keys(meta).length ? JSON.stringify(meta, null, 2) : '';
|
||||
return `${timestamp} [${level}]: ${message} ${metaStr}`;
|
||||
})
|
||||
);
|
||||
|
||||
// Custom format for file output
|
||||
const fileFormat = winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
);
|
||||
|
||||
// Create logger instance
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
defaultMeta: { service: 'ticket-microservice' },
|
||||
transports: [
|
||||
// Console transport
|
||||
new winston.transports.Console({
|
||||
format: consoleFormat
|
||||
}),
|
||||
|
||||
// File transport for all logs
|
||||
new winston.transports.File({
|
||||
filename: path.join(logsDir, 'app.log'),
|
||||
format: fileFormat,
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5
|
||||
}),
|
||||
|
||||
// File transport for errors only
|
||||
new winston.transports.File({
|
||||
filename: path.join(logsDir, 'error.log'),
|
||||
level: 'error',
|
||||
format: fileFormat,
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// Add request logging helper
|
||||
logger.logRequest = (req, res, responseTime) => {
|
||||
const logData = {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
statusCode: res.statusCode,
|
||||
responseTime: `${responseTime}ms`,
|
||||
userAgent: req.get('User-Agent'),
|
||||
ip: req.ip || req.connection.remoteAddress
|
||||
};
|
||||
|
||||
if (res.statusCode >= 400) {
|
||||
logger.warn('HTTP Request', logData);
|
||||
} else {
|
||||
logger.info('HTTP Request', logData);
|
||||
}
|
||||
};
|
||||
|
||||
// Add purchase logging helper
|
||||
logger.logPurchase = (eventId, ticketId, purchaseId, success, error = null) => {
|
||||
const logData = {
|
||||
eventId,
|
||||
ticketId,
|
||||
purchaseId,
|
||||
success,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (error) {
|
||||
logData.error = error.message || error;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
logger.info('Ticket Purchase', logData);
|
||||
} else {
|
||||
logger.error('Ticket Purchase Failed', logData);
|
||||
}
|
||||
};
|
||||
|
||||
// Add fallback logging helper
|
||||
logger.logFallback = (operation, reason) => {
|
||||
logger.warn('Fallback Activated', {
|
||||
operation,
|
||||
reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = logger;
|
||||
@@ -0,0 +1,247 @@
|
||||
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.ensureOutputDirectory();
|
||||
}
|
||||
|
||||
ensureOutputDirectory() {
|
||||
if (!fs.existsSync(this.outputDir)) {
|
||||
fs.mkdirSync(this.outputDir, { recursive: true });
|
||||
logger.info(`Created PDF output directory: ${this.outputDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
async generateTicketPDF(ticketData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const {
|
||||
ticketId,
|
||||
eventId,
|
||||
purchaseId,
|
||||
eventName,
|
||||
eventDescription,
|
||||
timestamp,
|
||||
soldCount
|
||||
} = ticketData;
|
||||
|
||||
// Create PDF document
|
||||
const doc = new PDFDocument({
|
||||
size: 'A4',
|
||||
margin: 50
|
||||
});
|
||||
|
||||
// Generate filename
|
||||
const filename = `ticket-${purchaseId}.pdf`;
|
||||
const filepath = path.join(this.outputDir, filename);
|
||||
|
||||
// Pipe to file
|
||||
const stream = fs.createWriteStream(filepath);
|
||||
doc.pipe(stream);
|
||||
|
||||
// Header
|
||||
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();
|
||||
|
||||
// Event Information
|
||||
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);
|
||||
|
||||
// Ticket Information
|
||||
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);
|
||||
|
||||
// QR Code placeholder (you could integrate a QR code library here)
|
||||
doc.rect(400, 250, 100, 100)
|
||||
.strokeColor('#bdc3c7')
|
||||
.lineWidth(1)
|
||||
.stroke();
|
||||
|
||||
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(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);
|
||||
|
||||
// Security watermark
|
||||
doc.fontSize(60)
|
||||
.fillColor('#ecf0f1')
|
||||
.text('VALID', 200, 300, {
|
||||
rotate: -45,
|
||||
opacity: 0.1
|
||||
});
|
||||
|
||||
// Finalize PDF
|
||||
doc.end();
|
||||
|
||||
stream.on('finish', () => {
|
||||
logger.info(`PDF ticket generated: ${filename}`);
|
||||
resolve({
|
||||
success: true,
|
||||
filename,
|
||||
filepath,
|
||||
size: fs.statSync(filepath).size
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('error', (error) => {
|
||||
logger.error('Error writing PDF file:', error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error generating PDF:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
results.push({
|
||||
success: false,
|
||||
ticketId: ticket.ticketId,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
getTicketPath(purchaseId) {
|
||||
return path.join(this.outputDir, `ticket-${purchaseId}.pdf`);
|
||||
}
|
||||
|
||||
ticketExists(purchaseId) {
|
||||
const filepath = this.getTicketPath(purchaseId);
|
||||
return fs.existsSync(filepath);
|
||||
}
|
||||
|
||||
async deleteTicket(purchaseId) {
|
||||
try {
|
||||
const filepath = this.getTicketPath(purchaseId);
|
||||
if (fs.existsSync(filepath)) {
|
||||
fs.unlinkSync(filepath);
|
||||
logger.info(`Deleted ticket PDF: ${purchaseId}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
logger.error(`Error deleting ticket PDF ${purchaseId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupOldTickets(maxAgeHours = 24) {
|
||||
try {
|
||||
const files = fs.readdirSync(this.outputDir);
|
||||
const now = Date.now();
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
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);
|
||||
|
||||
if (ageHours > maxAgeHours) {
|
||||
fs.unlinkSync(filepath);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount > 0) {
|
||||
logger.info(`Cleaned up ${deletedCount} old ticket PDFs`);
|
||||
}
|
||||
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
logger.error('Error cleaning up old tickets:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getStats() {
|
||||
try {
|
||||
const files = fs.readdirSync(this.outputDir);
|
||||
const pdfFiles = files.filter(f => f.endsWith('.pdf'));
|
||||
|
||||
let totalSize = 0;
|
||||
for (const file of pdfFiles) {
|
||||
const filepath = path.join(this.outputDir, file);
|
||||
totalSize += fs.statSync(filepath).size;
|
||||
}
|
||||
|
||||
return {
|
||||
totalTickets: pdfFiles.length,
|
||||
totalSizeBytes: totalSize,
|
||||
totalSizeMB: (totalSize / (1024 * 1024)).toFixed(2),
|
||||
outputDirectory: this.outputDir
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error getting PDF stats:', error);
|
||||
return {
|
||||
totalTickets: 0,
|
||||
totalSizeBytes: 0,
|
||||
totalSizeMB: '0.00',
|
||||
outputDirectory: this.outputDir,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new PDFGenerator();
|
||||
@@ -0,0 +1,185 @@
|
||||
const redis = require('redis');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const logger = require('./logger');
|
||||
|
||||
class RedisClient {
|
||||
constructor() {
|
||||
this.client = null;
|
||||
this.isConnected = false;
|
||||
this.luaScripts = {};
|
||||
this.loadLuaScripts();
|
||||
}
|
||||
|
||||
async connect() {
|
||||
try {
|
||||
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
|
||||
this.client = redis.createClient({ url: redisUrl });
|
||||
|
||||
this.client.on('error', (err) => {
|
||||
logger.error('Redis Client Error:', err);
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
this.client.on('connect', () => {
|
||||
logger.info('Redis client connected');
|
||||
this.isConnected = true;
|
||||
});
|
||||
|
||||
this.client.on('disconnect', () => {
|
||||
logger.warn('Redis client disconnected');
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
await this.client.connect();
|
||||
return this.client;
|
||||
} catch (error) {
|
||||
logger.error('Failed to connect to Redis:', error);
|
||||
this.isConnected = false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
loadLuaScripts() {
|
||||
try {
|
||||
const luaDir = path.join(__dirname, '../lua');
|
||||
|
||||
// Load purchase ticket script
|
||||
const purchaseScript = fs.readFileSync(
|
||||
path.join(luaDir, 'purchase-ticket.lua'),
|
||||
'utf8'
|
||||
);
|
||||
this.luaScripts.purchaseTicket = purchaseScript;
|
||||
|
||||
// Load event stats script
|
||||
const statsScript = fs.readFileSync(
|
||||
path.join(luaDir, 'get-event-stats.lua'),
|
||||
'utf8'
|
||||
);
|
||||
this.luaScripts.getEventStats = statsScript;
|
||||
|
||||
logger.info('Lua scripts loaded successfully');
|
||||
} catch (error) {
|
||||
logger.error('Failed to load Lua scripts:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async purchaseTicket(eventId, purchaseId, timestamp) {
|
||||
if (!this.isConnected) {
|
||||
throw new Error('Redis not connected');
|
||||
}
|
||||
|
||||
const keys = [
|
||||
`event:${eventId}:tickets`,
|
||||
`event:${eventId}:meta`,
|
||||
'global:stats'
|
||||
];
|
||||
const args = [timestamp, purchaseId];
|
||||
|
||||
try {
|
||||
const result = await this.client.eval(
|
||||
this.luaScripts.purchaseTicket,
|
||||
{ keys, arguments: args }
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Error executing purchase ticket script:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getEventStats(eventId) {
|
||||
if (!this.isConnected) {
|
||||
throw new Error('Redis not connected');
|
||||
}
|
||||
|
||||
const keys = [
|
||||
`event:${eventId}:meta`,
|
||||
`event:${eventId}:tickets`
|
||||
];
|
||||
|
||||
try {
|
||||
const result = await this.client.eval(
|
||||
this.luaScripts.getEventStats,
|
||||
{ keys }
|
||||
);
|
||||
|
||||
if (!result) return null;
|
||||
|
||||
return {
|
||||
eventId: result[0],
|
||||
name: result[1],
|
||||
description: result[2],
|
||||
totalTickets: parseInt(result[3]),
|
||||
soldTickets: parseInt(result[4]),
|
||||
remainingTickets: parseInt(result[5]),
|
||||
createdAt: result[6],
|
||||
lastSoldAt: result[7]
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error executing get event stats script:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getAllEvents() {
|
||||
if (!this.isConnected) {
|
||||
throw new Error('Redis not connected');
|
||||
}
|
||||
|
||||
try {
|
||||
const eventKeys = await this.client.keys('event:*:meta');
|
||||
const events = [];
|
||||
|
||||
for (const key of eventKeys) {
|
||||
const eventId = key.match(/event:(\d+):meta/)[1];
|
||||
const stats = await this.getEventStats(eventId);
|
||||
if (stats) {
|
||||
events.push(stats);
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
} catch (error) {
|
||||
logger.error('Error getting all events:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getGlobalStats() {
|
||||
if (!this.isConnected) {
|
||||
throw new Error('Redis not connected');
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await this.client.hGetAll('global:stats');
|
||||
return {
|
||||
totalEvents: parseInt(stats.totalEvents) || 0,
|
||||
totalTickets: parseInt(stats.totalTickets) || 0,
|
||||
totalSold: parseInt(stats.totalSold) || 0,
|
||||
lastSeeded: stats.lastSeeded || null
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error getting global stats:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getClient() {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
isHealthy() {
|
||||
return this.isConnected && this.client && this.client.isReady;
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
if (this.client) {
|
||||
await this.client.disconnect();
|
||||
this.isConnected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new RedisClient();
|
||||
Reference in New Issue
Block a user