Compare commits

...

2 Commits

Author SHA1 Message Date
Ayobami 43ae10d7dd feat: add project documentation 2025-07-31 21:38:18 +01:00
Ayobami 064ae104f7 feat: add autocannon and fix purchase ticket flow 2025-07-30 22:31:34 +01:00
12 changed files with 759 additions and 120 deletions
+19
View File
@@ -0,0 +1,19 @@
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.nyc_output
coverage
.DS_Store
*.log
logs/*
tickets/*
.vscode
.idea
*.md
!design.md
Dockerfile
docker-compose.yml
prometheus.yml
-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
+3
View File
@@ -1,3 +1,6 @@
/node_modules /node_modules
/dist /dist
/package-lock.json /package-lock.json
/tickets
/logs
.env
+37
View File
@@ -0,0 +1,37 @@
# Use official Node.js runtime as base image
FROM node:18-alpine
# Set working directory in container
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY . .
# Create necessary directories
RUN mkdir -p logs tickets
# Create non-root user for security
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
# Change ownership of app directory
RUN chown -R nodejs:nodejs /app
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 3049
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3049/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))"
# Start the application
CMD ["npm", "start"]
+110 -16
View File
@@ -42,34 +42,128 @@ Your task is to extract the high-throughput ticket purchasing component and exte
### Prerequisites ### Prerequisites
- Node.js (v14+ recommended) - Node.js (v18+ recommended)
- npm - npm or yarn
- Redis (installed locally or via Docker, as per the provided docker-compose configuration) - Docker and Docker Compose
- Git
### Setup ### Quick Start
1. Clone the repository. 1. **Clone the repository**
2. Install dependencies: ```bash
git clone <repository-url>
cd module4_backend_project
```
2. **Install dependencies**
```bash
npm install npm install
3. (Optional) Copy the environment variable template: ```
3. **Set up environment**
```bash
cp .env.example .env cp .env.example .env
4. Seed the Redis store with tickets for multiple events. You might modify the seeding script to handle multiple event keys (e.g., `event:1:tickets`, `event:2:tickets`, etc.). # Edit .env file if needed
5. Start the application: ```
npm start
4. **Start with Docker (Recommended)**
```bash
# Start core services (Redis + App)
docker-compose up -d
# Or start with monitoring (Prometheus + Grafana)
docker-compose --profile monitoring up -d
```
5. **Seed the database**
```bash
# Seed 5 events with 10,000 tickets each
npm run seed
# Custom seeding: 3 events with 5,000 tickets each
npm run seed 3 5000
```
### Manual Setup (Development)
If you prefer to run components separately:
1. **Start Redis**
```bash
docker-compose up -d redis
```
2. **Start the application**
```bash
npm run dev # Development with auto-reload
# or
npm start # Production mode
```
### API Endpoints
Once running, the following endpoints are available:
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/health` | System health check |
| GET | `/events` | List all events with statistics |
| GET | `/events/:eventId` | Get specific event details |
| POST | `/buy/:eventId` | Purchase a ticket for an event |
| GET | `/tickets/:purchaseId` | Download ticket PDF |
| GET | `/metrics` | Real-time system metrics |
| GET | `/admin/pdf-stats` | PDF management statistics |
| POST | `/admin/cleanup-tickets` | Cleanup old ticket files |
### Load Testing ### Load Testing
Simulate high load using a tool like [autocannon](https://github.com/mcollina/autocannon) or [wrk](https://github.com/wg/wrk). For example, to simulate 5000 concurrent connections on event 1: The system includes a comprehensive load testing framework:
npx autocannon -c 5000 -d 30 http://localhost:3049/buy/1 ```bash
# Run full test suite (5000+ concurrent connections)
npm run test:load -- --full
### Metrics # Test specific event
npm run test:load -- --event 1 --connections 5000 --duration 30
Access real-time service metrics at: # Multi-event concurrent testing
npm run test:load -- --multi --events 1,2,3 --connections 6000
http://localhost:3049/metrics # Custom load test
node tests/load-test.js --event 2 --connections 1000 --duration 10
```
These metrics should include data on tickets sold, remaining tickets per event, and any instances where the fallback mechanism was activated. ### Monitoring & Metrics
#### Application Metrics
Access real-time service metrics at: http://localhost:3049/metrics
#### Prometheus (if enabled)
Prometheus dashboard: http://localhost:9090
#### Grafana (if enabled)
Grafana dashboard: http://localhost:3000
- Username: `admin`
- Password: `admin`
### Docker Commands
```bash
# Start core services
docker-compose up -d
# Start with monitoring
docker-compose --profile monitoring up -d
# View logs
docker-compose logs -f app
# Stop services
docker-compose down
# Rebuild and restart
docker-compose up -d --build
```
## Evaluation Criteria ## Evaluation Criteria
+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();
+329
View File
@@ -0,0 +1,329 @@
# Ticket Scaling Microservice - Design Document
## Table of Contents
1. [Architecture Overview](#architecture-overview)
2. [System Components](#system-components)
3. [Scalability Strategies](#scalability-strategies)
4. [Atomic Operations](#atomic-operations)
5. [Fallback Mechanisms](#fallback-mechanisms)
6. [Performance Optimizations](#performance-optimizations)
7. [Monitoring & Observability](#monitoring--observability)
8. [Security Considerations](#security-considerations)
9. [Deployment Strategy](#deployment-strategy)
10. [Future Enhancements](#future-enhancements)
## Architecture Overview
### High-Level Architecture
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Load Balancer │ │ Prometheus │ │ Grafana │
│ (Optional) │ │ Monitoring │ │ Dashboard │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ │ │
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ Ticket Service │◄───┤ Redis │ │ In-Memory │
│ (Node.js/ │ │ Primary Store │ │ Fallback Store │
│ Express) │ │ │ │ │
│ │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────┐
│ PDF Generator │
│ (PDFKit) │
└─────────────────┘
```
### Design Principles
1. **High Availability**: Fallback mechanisms ensure service continuity
2. **Atomic Operations**: Redis Lua scripts prevent race conditions
3. **Horizontal Scalability**: Stateless design enables easy scaling
4. **Observability**: Comprehensive logging and metrics
5. **Performance**: Optimized for high-throughput scenarios
## System Components
### 1. Core Application (server.js)
- **Technology**: Node.js with Express framework
- **Responsibilities**:
- HTTP request handling
- Business logic orchestration
- Error handling and logging
- PDF generation coordination
### 2. Redis Client (redis-client.js)
- **Technology**: Redis with Lua scripting
- **Responsibilities**:
- Atomic ticket operations
- Event metadata management
- Connection health monitoring
- Script execution
### 3. Fallback Store (fallback-store.js)
- **Technology**: In-memory JavaScript Map
- **Responsibilities**:
- Emergency ticket storage
- Temporary operation continuity
- Graceful degradation
### 4. PDF Generator (pdf-generator.js)
- **Technology**: PDFKit library
- **Responsibilities**:
- Professional ticket generation
- File management
- Cleanup operations
### 5. Logging System (logger.js)
- **Technology**: Winston logging framework
- **Responsibilities**:
- Structured logging
- Request tracking
- Error reporting
- Performance metrics
## Scalability Strategies
### Horizontal Scaling
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Instance 1 │ │ Instance 2 │ │ Instance N │
│ Port: 3049 │ │ Port: 3050 │ │ Port: 305X │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┼───────────────────────┘
┌─────────────────┐
│ Shared Redis │
│ Cluster │
└─────────────────┘
```
**Key Features**:
- Stateless application design
- Shared Redis backend
- Load balancer distribution
- Independent scaling
### Vertical Scaling
- **CPU**: Multi-core utilization through Node.js cluster mode
- **Memory**: Configurable heap sizes for high-throughput
- **I/O**: Async operations prevent blocking
### Database Scaling
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Redis Master │ │ Redis Replica │ │ Redis Replica │
│ (Read/Write) │───▶│ (Read Only) │ │ (Read Only) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
**Strategies**:
- Redis clustering for horizontal scaling
- Read replicas for metrics/stats queries
- Sharding by event ID for massive scale
## Atomic Operations
### Lua Script Design
Our core purchase operation uses a Redis Lua script to ensure atomicity:
```lua
-- Atomic ticket purchase script
local ticketKey = KEYS[1] -- event:X:tickets
local metaKey = KEYS[2] -- event:X:meta
local globalKey = KEYS[3] -- global:stats
-- Atomic operations:
1. Check event exists
2. Pop ticket from list
3. Update sold count
4. Update global stats
5. Store purchase record
```
**Benefits**:
- **Race Condition Prevention**: All operations execute atomically
- **Consistency**: No partial state updates
- **Performance**: Single round-trip to Redis
- **Reliability**: All-or-nothing execution
### Concurrency Handling
- **Optimistic Locking**: Lua scripts handle concurrent access
- **Queue Management**: Redis lists provide FIFO ticket distribution
- **Connection Pooling**: Efficient Redis connection reuse
## Fallback Mechanisms
### Activation Triggers
1. **Redis Connection Failure**: Network issues or Redis downtime
2. **Script Execution Errors**: Lua script failures
3. **Timeout Scenarios**: Slow Redis responses
### Fallback Architecture
```
┌─────────────────┐
│ Request Comes │
└─────────────────┘
┌─────────────────┐ ┌─────────────────┐
│ Try Redis │───▶│ Redis Success │
│ Operation │ │ Return Result │
└─────────────────┘ └─────────────────┘
▼ (On Failure)
┌─────────────────┐ ┌─────────────────┐
│ Activate │───▶│ In-Memory │
│ Fallback Store │ │ Operation │
└─────────────────┘ └─────────────────┘
```
### Fallback Limitations
- **Non-Persistent**: Data lost on restart
- **Single Instance**: No cross-instance synchronization
- **Capacity Limited**: Memory constraints
- **Warning Logs**: Clear indication of degraded mode
## Performance Optimizations
### Application Level
1. **Async Operations**: Non-blocking I/O throughout
2. **Connection Pooling**: Reuse Redis connections
3. **Batch Operations**: Bulk ticket seeding
4. **Caching**: Event metadata caching
### Redis Optimizations
1. **Lua Scripts**: Reduced network round-trips
2. **Pipeline Operations**: Batch commands
3. **Memory Management**: Efficient data structures
4. **Persistence**: AOF for durability
### PDF Generation
1. **Async Generation**: Non-blocking PDF creation
2. **Stream Processing**: Memory-efficient file handling
3. **Cleanup Jobs**: Automatic old file removal
4. **Error Isolation**: PDF failures don't affect purchases
## Monitoring & Observability
### Metrics Collection
```json
{
"global": {
"totalEvents": 5,
"totalTickets": 50000,
"totalSold": 1250
},
"events": [
{
"eventId": "1",
"soldTickets": 250,
"remainingTickets": 9750
}
],
"system": {
"usingFallback": false,
"redisConnected": true,
"uptime": 3600,
"memoryUsage": {...}
},
"pdf": {
"totalTickets": 1250,
"totalSizeMB": "15.6"
}
}
```
### Logging Strategy
- **Structured Logging**: JSON format for parsing
- **Request Tracking**: Unique IDs for tracing
- **Performance Metrics**: Response times and throughput
- **Error Categorization**: Different log levels
### Health Checks
- **Application Health**: `/health` endpoint
- **Redis Connectivity**: Connection status
- **Fallback Status**: Degraded mode indication
- **Resource Usage**: Memory and CPU monitoring
## Security Considerations
### Input Validation
- **Event ID Validation**: Numeric constraints
- **Request Rate Limiting**: DDoS protection
- **Parameter Sanitization**: Injection prevention
### Container Security
- **Non-Root User**: Principle of least privilege
- **Minimal Base Image**: Alpine Linux for smaller attack surface
- **Health Checks**: Container monitoring
### Data Protection
- **No Sensitive Data**: Tickets are identifiers only
- **Audit Logging**: Purchase tracking
- **Secure Defaults**: Production-ready configuration
## Deployment Strategy
### Development Environment
```bash
# Local development
npm install
npm run docker:up # Start Redis
npm run seed # Seed events
npm run dev # Start with nodemon
```
### Production Environment
```bash
# Docker deployment
docker-compose up -d # Core services
docker-compose --profile monitoring up # With monitoring
```
### Container Orchestration
- **Docker Compose**: Local and small deployments
- **Kubernetes**: Large-scale deployments
- **Health Checks**: Automatic restart on failure
- **Resource Limits**: CPU and memory constraints
## Future Enhancements
### Performance Improvements
1. **Redis Clustering**: Horizontal database scaling
2. **CDN Integration**: PDF delivery optimization
3. **Caching Layer**: Application-level caching
4. **Connection Optimization**: Advanced pooling
### Feature Additions
1. **QR Code Generation**: Enhanced ticket security
2. **Email Integration**: Automatic ticket delivery
3. **Payment Processing**: Complete purchase flow
4. **Event Management**: Dynamic event creation
### Monitoring Enhancements
1. **Distributed Tracing**: Request flow tracking
2. **Custom Dashboards**: Business metrics visualization
3. **Alerting**: Proactive issue detection
4. **Performance Profiling**: Bottleneck identification
### Security Hardening
1. **Authentication**: API key management
2. **Rate Limiting**: Advanced throttling
3. **Encryption**: Data in transit protection
4. **Audit Trails**: Comprehensive logging
## Conclusion
This design provides a robust, scalable foundation for high-volume ticket sales with the following key strengths:
- **Atomic Operations**: Guaranteed consistency under load
- **High Availability**: Graceful degradation capabilities
- **Observability**: Comprehensive monitoring and logging
- **Scalability**: Horizontal and vertical scaling support
- **Performance**: Optimized for high-throughput scenarios
The architecture successfully handles the challenge requirements of processing thousands of concurrent requests while maintaining data integrity and system reliability.
+71 -1
View File
@@ -1,10 +1,80 @@
version: "3" version: "3.8"
services: services:
redis: redis:
image: redis:alpine image: redis:alpine
container_name: ticket-redis
ports: ports:
- "6379:6379" - "6379:6379"
volumes: volumes:
- redis_data:/data - redis_data:/data
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
networks:
- ticket-network
app:
build: .
container_name: ticket-microservice
ports:
- "3049:3049"
environment:
- NODE_ENV=production
- REDIS_URL=redis://redis:6379
- PORT=3049
- LOG_LEVEL=info
depends_on:
redis:
condition: service_healthy
volumes:
- ./logs:/app/logs
- ./tickets:/app/tickets
networks:
- ticket-network
restart: unless-stopped
prometheus:
image: prom/prometheus:latest
container_name: ticket-prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--storage.tsdb.retention.time=200h'
- '--web.enable-lifecycle'
networks:
- ticket-network
profiles:
- monitoring
grafana:
image: grafana/grafana:latest
container_name: ticket-grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
networks:
- ticket-network
profiles:
- monitoring
volumes: volumes:
redis_data: redis_data:
prometheus_data:
grafana_data:
networks:
ticket-network:
driver: bridge
+19
View File
@@ -0,0 +1,19 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'ticket-microservice'
static_configs:
- targets: ['app:3049']
metrics_path: '/metrics'
scrape_interval: 5s
scrape_timeout: 3s
+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)
+95 -75
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
.lineTo(545, 90) .moveTo(50, 90)
.strokeColor('#3498db') .lineTo(545, 90)
.lineWidth(2) .strokeColor("#3498db")
.stroke(); .lineWidth(2)
.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)
.text(`Event Name: ${eventName || `Event ${eventId}`}`, 50, 150) .fillColor("#34495e")
.text(`Event ID: ${eventId}`, 50, 170) .text(`Event Name: ${eventName || `Event ${eventId}`}`, 50, 150)
.text(`Description: ${eventDescription || 'No description available'}`, 50, 190); .text(`Event ID: ${eventId}`, 50, 170)
.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)
.text(`Ticket ID: ${ticketId}`, 50, 260) .fillColor("#34495e")
.text(`Purchase ID: ${purchaseId}`, 50, 280) .text(`Ticket ID: ${ticketId}`, 50, 260)
.text(`Purchase Date: ${new Date(timestamp).toLocaleString()}`, 50, 300) .text(`Purchase ID: ${purchaseId}`, 50, 280)
.text(`Ticket Number: #${soldCount}`, 50, 320); .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) // 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)
.lineWidth(1) .strokeColor("#bdc3c7")
.stroke(); .lineWidth(1)
.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)
.text(`Generated on ${new Date().toLocaleString()}`, 50, 520) .fillColor("#95a5a6")
.text('Powered by Ticket Microservice', 50, 535) .text(`Generated on ${new Date().toLocaleString()}`, 50, 520)
.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 // Security watermark
doc.fontSize(60) doc.fontSize(60).fillColor("#ecf0f1").text("VALID", 200, 300, {
.fillColor('#ecf0f1') rotate: -45,
.text('VALID', 200, 300, { opacity: 0.1,
rotate: -45, });
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;
} }
} }