From 60fcc713e3c667a38916b0d5dbfc74d12b23af29 Mon Sep 17 00:00:00 2001 From: Avnish Raut Date: Tue, 3 Mar 2026 14:08:07 +0100 Subject: [PATCH 1/3] R18 and R19 - Report generation --- .../src/reports/reports.controller.spec.ts | 18 +++ backend/src/reports/reports.controller.ts | 42 +++++ backend/src/reports/reports.module.ts | 9 +- backend/src/reports/reports.service.spec.ts | 18 +++ backend/src/reports/reports.service.ts | 148 ++++++++++++++++++ 5 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 backend/src/reports/reports.controller.spec.ts create mode 100644 backend/src/reports/reports.controller.ts create mode 100644 backend/src/reports/reports.service.spec.ts create mode 100644 backend/src/reports/reports.service.ts diff --git a/backend/src/reports/reports.controller.spec.ts b/backend/src/reports/reports.controller.spec.ts new file mode 100644 index 0000000..f3c142b --- /dev/null +++ b/backend/src/reports/reports.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ReportsController } from './reports.controller'; + +describe('ReportsController', () => { + let controller: ReportsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ReportsController], + }).compile(); + + controller = module.get(ReportsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/reports/reports.controller.ts b/backend/src/reports/reports.controller.ts new file mode 100644 index 0000000..ded4369 --- /dev/null +++ b/backend/src/reports/reports.controller.ts @@ -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('events/:id/report') + @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(); + } +} diff --git a/backend/src/reports/reports.module.ts b/backend/src/reports/reports.module.ts index a886549..aee94d4 100644 --- a/backend/src/reports/reports.module.ts +++ b/backend/src/reports/reports.module.ts @@ -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 {} diff --git a/backend/src/reports/reports.service.spec.ts b/backend/src/reports/reports.service.spec.ts new file mode 100644 index 0000000..79b4fa0 --- /dev/null +++ b/backend/src/reports/reports.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ReportsService } from './reports.service'; + +describe('ReportsService', () => { + let service: ReportsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ReportsService], + }).compile(); + + service = module.get(ReportsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/reports/reports.service.ts b/backend/src/reports/reports.service.ts new file mode 100644 index 0000000..c2e7fe1 --- /dev/null +++ b/backend/src/reports/reports.service.ts @@ -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 }, + }); + } +} From c62a5f6e07a7e9fec65ad6963762e40578fe5bd1 Mon Sep 17 00:00:00 2001 From: Avnish Raut Date: Wed, 4 Mar 2026 19:54:59 +0100 Subject: [PATCH 2/3] Changes as per review --- backend/src/reports/reports.controller.spec.ts | 8 ++++---- backend/src/reports/reports.controller.ts | 2 +- backend/src/reports/reports.service.spec.ts | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/src/reports/reports.controller.spec.ts b/backend/src/reports/reports.controller.spec.ts index f3c142b..9a32359 100644 --- a/backend/src/reports/reports.controller.spec.ts +++ b/backend/src/reports/reports.controller.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ReportsController } from './reports.controller'; +import { ReportController } from './reports.controller'; describe('ReportsController', () => { - let controller: ReportsController; + let controller: ReportController; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - controllers: [ReportsController], + controllers: [ReportController], }).compile(); - controller = module.get(ReportsController); + controller = module.get(ReportController); }); it('should be defined', () => { diff --git a/backend/src/reports/reports.controller.ts b/backend/src/reports/reports.controller.ts index ded4369..5833432 100644 --- a/backend/src/reports/reports.controller.ts +++ b/backend/src/reports/reports.controller.ts @@ -15,7 +15,7 @@ export class ReportController { * R18 – Generate Event Report * Only Organizer can request report for their own event */ - @Post('events/:id/report') + @Post('generate/report') @Roles(Role.ORG) create(@Param('id') id: string, @Req() req) { return this.service.createReport(+id, req.user.sub); diff --git a/backend/src/reports/reports.service.spec.ts b/backend/src/reports/reports.service.spec.ts index 79b4fa0..8548749 100644 --- a/backend/src/reports/reports.service.spec.ts +++ b/backend/src/reports/reports.service.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ReportsService } from './reports.service'; +import { ReportService } from './reports.service'; describe('ReportsService', () => { - let service: ReportsService; + let service: ReportService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [ReportsService], + providers: [ReportService], }).compile(); - service = module.get(ReportsService); + service = module.get(ReportService); }); it('should be defined', () => { From 20c51ca0d3cf75fc0818944d90297acc703d956b Mon Sep 17 00:00:00 2001 From: Avnish Raut Date: Wed, 4 Mar 2026 19:57:30 +0100 Subject: [PATCH 3/3] Changes as per review --- backend/src/reports/reports.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/reports/reports.controller.ts b/backend/src/reports/reports.controller.ts index 5833432..79eb28f 100644 --- a/backend/src/reports/reports.controller.ts +++ b/backend/src/reports/reports.controller.ts @@ -15,7 +15,7 @@ export class ReportController { * R18 – Generate Event Report * Only Organizer can request report for their own event */ - @Post('generate/report') + @Post(':id/report/generate') @Roles(Role.ORG) create(@Param('id') id: string, @Req() req) { return this.service.createReport(+id, req.user.sub);