Skip to content
Merged
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
21 changes: 19 additions & 2 deletions src/controllers/event-ticket.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,26 @@ import {
} from '../validators/event.validator';
import { UserAuthenticatedReq } from '../utils/types';
import { PaymentPrivacyDisclosureService } from '../services/payment-privacy-disclosure.service';
import { decodePaginationCursor } from '../utils/pagination-cursor';

export const getEventTickets: RequestHandler = async (req, res) => {
try {
const rawCursor = req.query.cursor;
const cursorString =
typeof rawCursor === 'string' ? rawCursor.trim() : undefined;
const cursor = cursorString ? decodePaginationCursor(cursorString) : null;
const page = parseInt(req.query.page as string, 10) || 1;
const limit = parseInt(req.query.limit as string, 10) || 8;

if (page < 1) {
if (cursorString && !cursor) {
return res.status(400).json({
error: 'Invalid cursor',
message:
'Cursor must be a valid pagination cursor generated by the API',
});
}

if (!cursor && page < 1) {
return res.status(400).json({
error: 'Invalid page number',
message: 'Page number must be greater than 0',
Expand All @@ -26,7 +39,11 @@ export const getEventTickets: RequestHandler = async (req, res) => {
});
}

const result = await EventTicketService.getEventTickets(page, limit);
const result = await EventTicketService.getEventTickets(
page,
limit,
cursor ?? undefined,
);

res.status(200).json(result);
} catch (error) {
Expand Down
27 changes: 27 additions & 0 deletions src/controllers/ticket-order.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { UserAuthenticatedReq } from '../utils/types';
import { getIdempotencyKey } from '../middlewares/idempotency';
import TicketOrder from '../models/ticket-order';
import EventTicket from '../models/event-ticket';
import { decodePaginationCursor } from '../utils/pagination-cursor';

/**
* Controller for Ticket Orders and Payments transparency
Expand All @@ -24,13 +25,26 @@ export const getUserOrders: RequestHandler = async (
});
}

const rawCursor = req.query.cursor;
const cursorString =
typeof rawCursor === 'string' ? rawCursor.trim() : undefined;
const cursor = cursorString ? decodePaginationCursor(cursorString) : null;
const page = parseInt(req.query.page as string, 10) || 1;
const limit = parseInt(req.query.limit as string, 10) || 10;

if (cursorString && !cursor) {
return res.status(400).json({
error: 'Invalid cursor',
message:
'Cursor must be a valid pagination cursor generated by the API',
});
}

const result = await TicketOrderService.getUserOrders(
userId.toString(),
page,
limit,
...(cursor ? [cursor] : []),
);

res.status(200).json({
Expand Down Expand Up @@ -63,13 +77,26 @@ export const getOrganizerOrders: RequestHandler = async (
});
}

const rawCursor = req.query.cursor;
const cursorString =
typeof rawCursor === 'string' ? rawCursor.trim() : undefined;
const cursor = cursorString ? decodePaginationCursor(cursorString) : null;
const page = parseInt(req.query.page as string, 10) || 1;
const limit = parseInt(req.query.limit as string, 10) || 10;

if (cursorString && !cursor) {
return res.status(400).json({
error: 'Invalid cursor',
message:
'Cursor must be a valid pagination cursor generated by the API',
});
}

const result = await TicketOrderService.getOrganizerOrders(
userId.toString(),
page,
limit,
...(cursor ? [cursor] : []),
);

res.status(200).json({
Expand Down
2 changes: 2 additions & 0 deletions src/models/event-ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export interface IEventTicket extends Document {
isPublished: boolean; // whether the event is published
allowAnonymous: boolean; // whether unauthenticated users may purchase tickets
requiresVerification: boolean; // whether attendees must have a verified email
createdAt?: Date;
updatedAt?: Date;
__v?: number; // Mongoose version key for optimistic locking
}

Expand Down
3 changes: 2 additions & 1 deletion src/routes/event-ticket.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ eventTicketRoutes.post('/scan', authGuard, scanTicket);
// POST /api/event-tickets/validate - Validate ticket without marking as used
eventTicketRoutes.post('/validate', authGuard, validateTicket);

// GET /api/event-tickets - Fetch paginated event tickets
// GET /api/event-tickets - Fetch tickets with cursor pagination via `cursor` and `limit`,
// or `page` and `limit` as a fallback. Responses include `nextCursor` when more data exists.
eventTicketRoutes.get('/', getEventTickets);

// GET /api/event-tickets/category/:category - Fetch event tickets by category
Expand Down
4 changes: 2 additions & 2 deletions src/routes/ticket-order.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ const ticketOrderRoutes = Router();

/**
* @route GET /ticket-orders/my-orders
* @desc Get ticket orders for the logged-in user
* @desc Get ticket orders for the logged-in user using `cursor` and `limit`, or `page` fallback
* @access Private
*/
ticketOrderRoutes.get('/my-orders', authGuard, getUserOrders);

/**
* @route GET /ticket-orders/organizer-orders
* @desc Get ticket orders for events organized by the logged-in user
* @desc Get ticket orders for events organized by the logged-in user using `cursor` and `limit`, or `page` fallback
* @access Private
*/
ticketOrderRoutes.get('/organizer-orders', authGuard, getOrganizerOrders);
Expand Down
61 changes: 52 additions & 9 deletions src/services/event-ticket.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import TicketOrder, { ITicketOrder } from '../models/ticket-order';
import User from '../models/user';
import Media from '../models/media';
import zkEmailNotificationService from './zk-email-notification.service';
import {
encodePaginationCursor,
PaginationCursor,
} from '../utils/pagination-cursor';

import { CreateEventStepTwoInput } from '../validators/event.validator';

Expand All @@ -25,6 +29,7 @@ export interface PaginatedEventTicketsResponse {
page: number;
limit: number;
total: number;
nextCursor?: string | null;
tickets: EventTicketResponse[];
}

Expand Down Expand Up @@ -153,6 +158,7 @@ export class EventTicketService {
page: validPage,
limit: validLimit,
total,
nextCursor: null,
tickets: transformedTickets,
};
} catch (error) {
Expand All @@ -168,34 +174,71 @@ export class EventTicketService {
static async getEventTickets(
page: number = 1,
limit: number = this.DEFAULT_LIMIT,
cursor?: PaginationCursor,
): Promise<PaginatedEventTicketsResponse> {
try {
// Validate pagination parameters
const validPage = Math.max(1, page);
const validLimit = Math.min(Math.max(1, limit), 50); // Cap at 50 for performance

// Calculate skip value
const skip = (validPage - 1) * validLimit;
const useCursor = Boolean(cursor);

const cursorFilter = cursor
? {
$or: [
{ createdAt: { $lt: cursor.sortValue } },
{
createdAt: cursor.sortValue,
_id: { $lt: new mongoose.Types.ObjectId(cursor.id) },
},
],
}
: {};

let query = EventTicket.find(cursorFilter).sort({
createdAt: -1,
_id: -1,
});

if (useCursor) {
query = query.limit(validLimit + 1);
} else {
const skip = (validPage - 1) * validLimit;
query = query.skip(skip).limit(validLimit);
}

// Get total count
const total = await EventTicket.countDocuments();

// Fetch tickets with pagination
const tickets = await EventTicket.find()
.sort({ createdAt: -1 }) // Sort by newest first
.skip(skip)
.limit(validLimit)
.lean(); // Use lean() for better performance
const tickets = await query.lean(); // Use lean() for better performance

const paginatedTickets = useCursor ? tickets.slice(0, validLimit) : tickets;

// Transform tickets to response format
const transformedTickets = tickets.map((ticket) =>
const transformedTickets = paginatedTickets.map((ticket) =>
this.transformEventTicket(ticket as unknown as IEventTicket),
);

const hasNextPage = useCursor && tickets.length > validLimit;
const nextCursor =
hasNextPage && paginatedTickets.length > 0
? encodePaginationCursor(
new Date(
(paginatedTickets[paginatedTickets.length - 1] as any)
.createdAt,
),
String(
(paginatedTickets[paginatedTickets.length - 1] as any)
._id,
),
)
: null;

return {
page: validPage,
limit: validLimit,
total,
nextCursor,
tickets: transformedTickets,
};
} catch (error) {
Expand Down
Loading
Loading