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
18 changes: 18 additions & 0 deletions backend/src/reports/reports.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ReportController } from './reports.controller';

describe('ReportsController', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the same issue

let controller: ReportController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ReportController],
}).compile();

controller = module.get<ReportController>(ReportController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
42 changes: 42 additions & 0 deletions backend/src/reports/reports.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Controller, Post, Get, Param, Req, UseGuards } from '@nestjs/common';
import { ReportService } from './reports.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { Role } from '@prisma/client';

@Controller()
@UseGuards(JwtAuthGuard, RolesGuard)
// 🔐 Role-based access control for report endpoints
export class ReportController {
constructor(private service: ReportService) {}

/**
* R18 – Generate Event Report
* Only Organizer can request report for their own event
*/
@Post(':id/report/generate')
@Roles(Role.ORG)
create(@Param('id') id: string, @Req() req) {
return this.service.createReport(+id, req.user.sub);
}

/**
* R18– View report status and results
* Organizer can view own reports
* Admin can view any report
*/
@Get('reports/:id')
get(@Param('id') id: string, @Req() req) {
return this.service.getReport(+id, req.user);
}

/**
* – Admin can view all reports
*/
@Get('reports')
@Roles(Role.ADMIN)
getAll() {
return this.service.getAllReports();
}
}
9 changes: 8 additions & 1 deletion backend/src/reports/reports.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Module } from '@nestjs/common';
import { ReportService } from './reports.service';
import { ReportController } from './reports.controller';
import { PrismaModule } from '../prisma/prisma.module';

@Module({})
@Module({
imports: [PrismaModule],
controllers: [ReportController],
providers: [ReportService],
})
export class ReportsModule {}
18 changes: 18 additions & 0 deletions backend/src/reports/reports.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ReportService } from './reports.service';

describe('ReportsService', () => {
let service: ReportService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ReportService],
}).compile();

service = module.get<ReportService>(ReportService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
148 changes: 148 additions & 0 deletions backend/src/reports/reports.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import {
Injectable,
NotFoundException,
ForbiddenException,
BadRequestException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { ReportStatus } from '@prisma/client';

@Injectable()
export class ReportService {
constructor(private prisma: PrismaService) {}

/**
* R18 – Create report request
* - Validates event existence
* - Validates organizer ownership
* - Initializes report as PENDING
*/
async createReport(eventId: number, organizerId: number) {
const event = await this.prisma.event.findUnique({
where: { event_id: eventId },
});

if (!event) throw new NotFoundException('Event not found');

// R18 – Ensure organizer owns the event
if (event.organizer_id !== organizerId)
throw new ForbiddenException('You do not own this event');

// Prevent duplicate reports (optional improvement)
const existing = await this.prisma.report.findFirst({
where: {
event_id: eventId,
status: { in: ['PENDING', 'IN_PROGRESS'] },
},
});

if (existing) throw new BadRequestException('Report already in progress');

const report = await this.prisma.report.create({
data: {
event_id: eventId,
organizer_id: organizerId,
status: ReportStatus.PENDING,
progress_percent: 0,
},
});

// R19 – Start simulated background processing
this.processReport(report.report_id);

return report;
}

/**
* R19 – Simulated async processing
* - Updates progress
* - Computes analytics
* - Stores JSON result
*/
private async processReport(reportId: number) {
// Move to IN_PROGRESS
await this.prisma.report.update({
where: { report_id: reportId },
data: { status: ReportStatus.IN_PROGRESS },
});

for (let percent = 10; percent <= 100; percent += 10) {
await new Promise((resolve) => setTimeout(resolve, 1500));

await this.prisma.report.update({
where: { report_id: reportId },
data: { progress_percent: percent },
});
}

// Fetch registrations to calculate analytics
const report = await this.prisma.report.findUnique({
where: { report_id: reportId },
include: {
event: {
include: { registrations: true },
},
},
});

if (!report) return; // Prevent null access

const total = report.event.registrations.length;

const confirmed = report.event.registrations.filter(
(r) => r.status === 'CONFIRMED',
).length;

const cancelled = report.event.registrations.filter(
(r) => r.status === 'CANCELLED',
).length;

const occupancyRate =
report.event.capacity > 0
? Math.round((confirmed / report.event.capacity) * 100)
: 0;

await this.prisma.report.update({
where: { report_id: reportId },
data: {
status: ReportStatus.DONE,
result_data: {
total_registrations: total,
confirmed_registrations: confirmed,
cancelled_registrations: cancelled,
capacity: report.event.capacity,
occupancy_rate_percent: occupancyRate,
},
},
});
}

/**
* R35 + R36 – View report status + result
* - Organizer can view own
* - Admin can view any
*/
async getReport(reportId: number, user: any) {
const report = await this.prisma.report.findUnique({
where: { report_id: reportId },
});

if (!report) throw new NotFoundException('Report not found');

// R37 – Access control validation
if (user.role !== 'ADMIN' && report.organizer_id !== user.sub) {
throw new ForbiddenException('Access denied');
}

return report;
}

/**
* R37 – Admin can view all reports
*/
async getAllReports() {
return this.prisma.report.findMany({
include: { event: true },
});
}
}