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
141 changes: 95 additions & 46 deletions backend/src/controllers/EnrollmentController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/**
* Enrollment Controller
* Handles enrollment-related operations and business logic
*
* Updated for Issue #257: uses cursor-based pagination with standard
* query parameters (limit, cursor, sort, order, filter).
*/

import { Request, Response, NextFunction } from 'express';
Expand All @@ -21,6 +24,18 @@ import {
import { UserRole } from '../models/User';
import logger from '../utils/logger';
import { NotFoundError, ForbiddenError, ValidationError, ConflictError } from '../utils/errors';
import {
parsePaginationParams,
parseFilters,
buildPaginationMeta,
buildPaginatedResponse,
decodeCursor,
registerSortFields,
resolveSortField,
} from '../utils/pagination';

// Register allowed sort fields for enrollments
registerSortFields('enrollments', ['enrolledAt', 'status', 'courseId', 'progress', 'createdAt', 'updatedAt']);

export class EnrollmentController {
private enrollmentService: EnrollmentService;
Expand All @@ -35,43 +50,64 @@ export class EnrollmentController {

/**
* Get user's enrollments with filtering and pagination
*
* Query params: limit, cursor, sort, order, status, paymentStatus, paymentMethod
*/
async getUserEnrollments(req: Request, res: Response, next: NextFunction) {
try {
const userId = req.user!.id;
const {
status,
paymentStatus,
paymentMethod,
page = '1',
limit = '10',
sortBy = 'enrolledAt',
sortOrder = 'desc'
} = req.query;
const pagination = parsePaginationParams(req.query as Record<string, unknown>);
const filters = parseFilters(req.query as Record<string, unknown>);

// Resolve the sort field against the allowlist
const sortField = resolveSortField('enrollments', pagination.sort);

// Build the enrollment filter with standardised pagination
const filter: EnrollmentFilter = {
userId,
status: status ? (Array.isArray(status) ? status as EnrollmentStatus[] : [status as EnrollmentStatus]) : undefined,
paymentStatus: paymentStatus ? (Array.isArray(paymentStatus) ? paymentStatus as PaymentStatus[] : [paymentStatus as PaymentStatus]) : undefined,
paymentMethod: paymentMethod ? (Array.isArray(paymentMethod) ? paymentMethod as PaymentMethod[] : [paymentMethod as PaymentMethod]) : undefined,
sortBy: sortBy as any,
sortOrder: sortOrder as 'asc' | 'desc',
page: parseInt(page as string),
limit: parseInt(limit as string)
status: filters.status
? (Array.isArray(filters.status) ? filters.status as EnrollmentStatus[] : [filters.status as EnrollmentStatus])
: undefined,
paymentStatus: req.query.paymentStatus
? (Array.isArray(req.query.paymentStatus)
? (req.query.paymentStatus as string[]) as PaymentStatus[]
: [req.query.paymentStatus as string as PaymentStatus])
: undefined,
paymentMethod: req.query.paymentMethod
? (Array.isArray(req.query.paymentMethod)
? (req.query.paymentMethod as string[]) as PaymentMethod[]
: [req.query.paymentMethod as string as PaymentMethod])
: undefined,
sortBy: sortField as any,
sortOrder: pagination.order,
// Map cursor to offset for the service layer
page: 1,
limit: pagination.limit + 1, // fetch one extra to determine has_more
};

const result = await this.enrollmentService.getEnrollments(filter);

res.json({
success: true,
data: result.enrollments,
pagination: {
page: result.page,
limit: result.limit,
total: result.total,
pages: Math.ceil(result.total / result.limit)
// Decode cursor to apply offset if present
if (pagination.cursor) {
const decoded = decodeCursor(pagination.cursor);
if (decoded) {
(filter as any).cursorValue = decoded;
(filter as any).cursorField = sortField;
}
});
}

const result = await this.enrollmentService.getEnrollments(filter);
const items = result.enrollments.slice(0, pagination.limit);
const hasMore = result.enrollments.length > pagination.limit;

const meta = buildPaginationMeta(
items as unknown as Record<string, unknown>[],
result.total,
pagination.limit,
sortField,
);
// Override has_more based on actual count
meta.has_more = hasMore;

res.json(buildPaginatedResponse(items as unknown as Record<string, unknown>[], meta));
} catch (error) {
logger.error('Error getting user enrollments:', error);
next(error);
Expand Down Expand Up @@ -371,35 +407,48 @@ export class EnrollmentController {

/**
* Get course enrollments (for educators/admins)
*
* Query params: limit, cursor, sort, order, status
*/
async getCourseEnrollments(req: Request, res: Response, next: NextFunction) {
try {
const { courseId } = req.params;
const {
status,
page = '1',
limit = '50'
} = req.query;
const pagination = parsePaginationParams(req.query as Record<string, unknown>);
const filters = parseFilters(req.query as Record<string, unknown>);
const sortField = resolveSortField('enrollments', pagination.sort);

const filter: EnrollmentFilter = {
courseId,
status: status ? (Array.isArray(status) ? status as EnrollmentStatus[] : [status as EnrollmentStatus]) : undefined,
page: parseInt(page as string),
limit: parseInt(limit as string)
status: filters.status
? (Array.isArray(filters.status) ? filters.status as EnrollmentStatus[] : [filters.status as EnrollmentStatus])
: undefined,
sortBy: sortField as any,
sortOrder: pagination.order,
page: 1,
limit: pagination.limit + 1,
};

const result = await this.enrollmentService.getEnrollments(filter);

res.json({
success: true,
data: result.enrollments,
pagination: {
page: result.page,
limit: result.limit,
total: result.total,
pages: Math.ceil(result.total / result.limit)
if (pagination.cursor) {
const decoded = decodeCursor(pagination.cursor);
if (decoded) {
(filter as any).cursorValue = decoded;
(filter as any).cursorField = sortField;
}
});
}

const result = await this.enrollmentService.getEnrollments(filter);
const items = result.enrollments.slice(0, pagination.limit);
const hasMore = result.enrollments.length > pagination.limit;

const meta = buildPaginationMeta(
items as unknown as Record<string, unknown>[],
result.total,
pagination.limit,
sortField,
);
meta.has_more = hasMore;

res.json(buildPaginatedResponse(items as unknown as Record<string, unknown>[], meta));
} catch (error) {
logger.error('Enrollment error:', error);
next(error);
Expand Down
Loading
Loading