Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions monolith/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"cors": "^2.8.5",
"express": "^5.1.0",
"ioredis": "^5.8.2",
"prettier": "^3.7.4",
"sqlite3": "^5.1.7",
"uuid": "^13.0.0"
},
Expand Down
193 changes: 126 additions & 67 deletions monolith/src/api/v2Routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Router, Request, Response } from "express";
import { v4 as uuidv4 } from "uuid";
import { EventService } from "../services/eventService";
import { TicketService } from "../services/ticketService";
import { LockService } from "../services/lockService";
import { paymentQueue } from "../queues/paymentQueue";
import { rateLimitMiddleware, idempotencyMiddleware, cacheMiddleware, lockMiddleware } from "../middleware/redisMiddleware";
import {locks} from "../services/redis"

const v2Routes = Router();

Expand Down Expand Up @@ -40,10 +41,14 @@ v2Routes.get("/", (req, res) => {

const eventService = new EventService();
const ticketService = new TicketService();
const lockService = new LockService();

// Event Routes (same as before)
v2Routes.get("/events", async (req: Request, res: Response) => {
// Cache all events for 5 minutes
v2Routes.get("/events",
cacheMiddleware({
ttl: 300,
keyPrefix: "events-all"
}), async (req: Request, res: Response) => {
try {
const events = await eventService.getAllEvents();
res.json(events);
Expand All @@ -65,82 +70,89 @@ v2Routes.get("/events/:id", async (req: Request, res: Response) => {
});

// IMPROVED Ticket Purchase Route with distributed locking
v2Routes.post("/tickets/purchase", async (req: Request, res: Response) => {
const { eventId, userId } = req.body;
const lockIdentifier = uuidv4();
const idempotencyKey = (req as any).idempotencyKey;

// Acquire lock for this event
const lockKey = `event:${eventId}:inventory`;
const lockAcquired = await lockService.waitForLock(
lockKey,
lockIdentifier,
5000
);

if (!lockAcquired) {
return res.status(503).json({
error: "Service temporarily unavailable. Please try again.",
});
}
v2Routes.post(
"/tickets/purchase",
rateLimitMiddleware({
max: 10,
windowMs: 60000 // 10 purchases per minute
}),
idempotencyMiddleware,
lockMiddleware((req: Request) => `ticket:${req.body.eventId}:${req.body.seatId}`),
async (req: Request, res: Response) => {
const { eventId, userId } = req.body;
const lockIdentifier = uuidv4();
const idempotencyKey = (req as any).idempotencyKey;

try {
console.log(`User ${userId} acquired lock for event ${eventId}`);
// Acquire lock for this event
const lockKey = `event:${eventId}:inventory`;
const lockAcquired = await locks.waitForLock(
lockKey,
5000
);

// Check event availability (now protected by lock!)
const event = await eventService.getEventById(eventId);
if (!event) {
return res.status(404).json({ error: "Event not found" });
if (!lockAcquired) {
return res.status(503).json({
error: "Service temporarily unavailable. Please try again.",
});
}

if (event.availableTickets <= 0) {
return res.status(400).json({ error: "No tickets available" });
}
try {
console.log(`User ${userId} acquired lock for event ${eventId}`);

console.log(
`Available tickets: ${event.availableTickets} (with lock protection)`
);
// Check event availability (now protected by lock!)
const event = await eventService.getEventById(eventId);
if (!event) {
return res.status(404).json({ error: "Event not found" });
}

// Create ticket and immediately decrease inventory
const ticket = await ticketService.createTicket(
eventId,
userId,
event.price
);
await eventService.updateEventTickets(eventId, event.availableTickets - 1);

// Queue payment processing (async)
await paymentQueue.add(
{
ticketId: ticket.id,
eventId: eventId,
userId: userId,
amount: event.price,
idempotencyKey: idempotencyKey,
},
{
delay: 0,
attempts: 3,
if (event.availableTickets <= 0) {
return res.status(400).json({ error: "No tickets available" });
}
);

console.log(
`Ticket ${ticket.id} created and payment queued for user ${userId}`
);
console.log(
`Available tickets: ${event.availableTickets} (with lock protection)`
);

res.status(202).json({
ticket,
message: "Ticket reserved. Payment is being processed.",
status: "processing",
});
// Create ticket and immediately decrease inventory
const ticket = await ticketService.createTicket(
eventId,
userId,
event.price
);
await eventService.updateEventTickets(eventId, event.availableTickets - 1);

// Queue payment processing (async)
await paymentQueue.add(
{
ticketId: ticket.id,
eventId: eventId,
userId: userId,
amount: event.price,
idempotencyKey: idempotencyKey,
},
{
delay: 0,
attempts: 3,
}
);

console.log(
`Ticket ${ticket.id} created and payment queued for user ${userId}`
);

res.status(202).json({
ticket,
message: "Ticket reserved. Payment is being processed.",
status: "processing",
});
} catch (error: any) {
console.error("Purchase failed:", error.message);
res
.status(500)
.json({ error: error.message || "Failed to purchase ticket" });
} finally {
// Always release the lock
await lockService.releaseLock(lockKey, lockIdentifier);
await locks.release(lockKey, lockIdentifier);
console.log(`Lock released for event ${eventId}`);
}
});
Expand Down Expand Up @@ -205,17 +217,17 @@ v2Routes.get("/admin/queue-stats", async (req: Request, res: Response) => {
v2Routes.get("/health", async (req: Request, res: Response) => {
try {
// Check Redis connection
const lockTest = await lockService.acquireLock("health-check", "test");
if (lockTest) {
await lockService.releaseLock("health-check", "test");
const lockId = await locks.acquire("health-check");
if (lockId) {
await locks.release("health-check", lockId);
}

res.json({
status: "ok",
timestamp: new Date(),
services: {
api: "healthy",
redis: lockTest ? "healthy" : "unhealthy",
redis: lockId ? "healthy" : "unhealthy",
queue: "healthy",
},
});
Expand All @@ -232,4 +244,51 @@ v2Routes.get("/health", async (req: Request, res: Response) => {
}
});

// // ========== Payment Processing with Idempotency ==========

// router.post(
// "/payments/process",
// idempotencyMiddleware,
// async (req: Request, res: Response) => {
// const { ticketId, paymentMethod, amount } = req.body;

// // This won't process twice for the same request
// const payment = await paymentService.processPayment({
// ticketId,
// paymentMethod,
// amount,
// idempotencyKey: (req as any).idempotencyKey
// });

// res.json(payment);
// }
// );

// v2Routes.put(
// "/admin/events/:id",
// async (req: Request, res: Response) => {
// const event = await eventService.updateEventTickets(req.params.id, req.body);

// // Clear specific caches
// await re.deleteCache(`event:${req.params.id}:*`);
// await redis.deleteCache("events-*");

// res.json(event);
// }
// );

// Cache search with shorter TTL and conditional caching
// router.get(
// "/events/search",
// cacheMiddleware({
// ttl: 120,
// keyPrefix: "events-search",
// condition: (req) => !req.query.availability // Don't cache availability searches
// }),
// async (req: Request, res: Response) => {
// const events = await eventService.searchEvents(req.query);
// res.json(events);
// }
// );

export { v2Routes };
20 changes: 13 additions & 7 deletions monolith/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { v1Routes } from "./api/v1Routes";
import { v2Routes } from "./api/v2Routes";
import errorHandler, { notFoundHandler } from "./middleware/errorHandler";
import { API_CONFIG } from "@ticketflow/shared/types";
import { idempotencyMiddleware } from "./middleware/idempotency";

const app = express();
const PORT = process.env.PORT || 3000;
Expand All @@ -19,9 +18,6 @@ app.use((req, res, next) => {
next();
});

// Apply idempotency middleware to all routes
app.use(idempotencyMiddleware);

// Routes
app.get("/", (req, res) => {
res.json({
Expand All @@ -44,11 +40,21 @@ app.use("/api/v2", v2Routes);

// Redirect /api to current version
app.use("/api", (req, res, next) => {
// Only redirect if path is exactly /api or /api/
if (req.path === "/" || req.path === "") {
res.redirect(`/api/${API_CONFIG.current}`);
} else {
res.redirect(`api/${API_CONFIG.current}${req.path}`);
return res.redirect(`/api/${API_CONFIG.current}`);
}

// Check if the path already includes a version
const versionPattern = /^\/v\d+/;
if (!versionPattern.test(req.path)) {
// Path doesn't include version, redirect to current version
return res.redirect(`/api/${API_CONFIG.current}${req.path}`);
}

// Path already includes version or doesn't match our pattern
// Let it fall through to 404 handler
next();
});

// 404 handler
Expand Down
50 changes: 0 additions & 50 deletions monolith/src/middleware/idempotency.ts

This file was deleted.

Loading