From e230a41aab9d65e84136e3469c44e40cf09da559 Mon Sep 17 00:00:00 2001 From: Wilmer Junior Santiago Donado Date: Thu, 28 May 2026 17:01:38 -0500 Subject: [PATCH 1/7] [CU-86e1kwwx0]: Change GET /evaluations/courses/id/top-projects to select n project in the top --- .../modules/evaluations/evaluations.controller.ts | 15 +++++++++++---- .../modules/evaluations/evaluations.service.ts | 6 +++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/gateway/src/modules/evaluations/evaluations.controller.ts b/apps/gateway/src/modules/evaluations/evaluations.controller.ts index 98988088..f58c6837 100644 --- a/apps/gateway/src/modules/evaluations/evaluations.controller.ts +++ b/apps/gateway/src/modules/evaluations/evaluations.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Post, Param, ParseIntPipe, Body, Query } from '@nestjs/common'; +import { Controller, Get, Post, Param, ParseIntPipe, DefaultValuePipe, Body, Query } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiSecurity, ApiBody, ApiQuery } from '@nestjs/swagger'; import { EvaluationsService } from './evaluations.service'; import { EvaluateProjectDto } from './dto/evaluate-project.dto'; @@ -77,9 +77,9 @@ export class EvaluationsController { @RequirePermission('manage:events') @Get('courses/:courseId/top-projects') @ApiOperation({ - summary: 'Get top 5 projects for a course', + summary: 'Get top N projects for a course', description: - 'Returns the top 5 projects for a specific course within an event, ranked by average evaluation grade. ' + + 'Returns the top N projects for a specific course within an event, ranked by average evaluation grade. ' + 'Each project includes complete project details along with average grade and evaluation count.', }) @ApiParam({ @@ -93,6 +93,12 @@ export class EvaluationsController { example: 1, required: true, }) + @ApiQuery({ + name: 'limit', + description: 'Number of top projects to return', + example: 5, + required: false, + }) @ApiResponse({ status: 200, description: 'Top projects retrieved successfully', @@ -104,8 +110,9 @@ export class EvaluationsController { async getTopProjectsByCourse( @Param('courseId', ParseIntPipe) courseId: number, @Query('eventId', ParseIntPipe) eventId: number, + @Query('limit', new DefaultValuePipe(5), ParseIntPipe) limit: number, ) { - return this.evaluationsService.getTopProjectsByCourse(courseId, eventId); + return this.evaluationsService.getTopProjectsByCourse(courseId, eventId, limit); } } diff --git a/apps/gateway/src/modules/evaluations/evaluations.service.ts b/apps/gateway/src/modules/evaluations/evaluations.service.ts index d0edd5ea..d799d041 100644 --- a/apps/gateway/src/modules/evaluations/evaluations.service.ts +++ b/apps/gateway/src/modules/evaluations/evaluations.service.ts @@ -154,7 +154,7 @@ export class EvaluationsService implements OnModuleInit { ); } - async getTopProjectsByCourse(courseId: number, eventId: number) { + async getTopProjectsByCourse(courseId: number, eventId: number, limit: number = 5) { // 1. Fetch all project IDs for the course and event from project service const projectsResponse = await lastValueFrom( this.projectsService.listProjectsByEvent({ @@ -184,10 +184,10 @@ export class EvaluationsService implements OnModuleInit { return { items: [], courseId, eventId }; } - // 3. Sort by averageGrade descending and take top 5 + // 3. Sort by averageGrade descending and take top N const topProjects = projectStats .sort((a, b) => b.averageGrade - a.averageGrade) - .slice(0, 5); + .slice(0, limit); // 4. Fetch full project details for the top 5 const topProjectIds = topProjects.map(tp => tp.projectId); From ec6ad2d2d727ec3d8e8ac4a8e40421ccf7046947 Mon Sep 17 00:00:00 2001 From: Wilmer Junior Santiago Donado Date: Thu, 28 May 2026 17:25:50 -0500 Subject: [PATCH 2/7] [CU-86e1kwwx0]: Return the projects according with Tiebreak On GET /evaluations/courses/id/top-projects we are checking if Tiebreak if exist anyone register on it --- .../modules/evaluations/evaluations.module.ts | 17 ++++++++++ .../evaluations/evaluations.service.ts | 31 ++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/apps/gateway/src/modules/evaluations/evaluations.module.ts b/apps/gateway/src/modules/evaluations/evaluations.module.ts index b1592297..dfcf8632 100644 --- a/apps/gateway/src/modules/evaluations/evaluations.module.ts +++ b/apps/gateway/src/modules/evaluations/evaluations.module.ts @@ -5,6 +5,7 @@ import { join } from 'path'; import { EVALUATION_SERVICE_NAME, CRITERIONS_SERVICE_NAME, + TIE_BREAK_SERVICE_NAME, protobufPackage as evaluationProtobufPackage, } from '@app/common/generated/evaluation'; import { @@ -49,6 +50,22 @@ import { EvaluationsService } from './evaluations.service'; }), inject: [ConfigService], }, + { + name: TIE_BREAK_SERVICE_NAME, + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + transport: Transport.GRPC, + options: { + package: evaluationProtobufPackage, + protoPath: join( + process.cwd(), + 'libs/common/src/protos/evaluation.proto', + ), + url: configService.get('EVALUATION_SERVICE_URL'), + }, + }), + inject: [ConfigService], + }, { name: PROJECTS_SERVICE_NAME, imports: [ConfigModule], diff --git a/apps/gateway/src/modules/evaluations/evaluations.service.ts b/apps/gateway/src/modules/evaluations/evaluations.service.ts index d799d041..ada77b8e 100644 --- a/apps/gateway/src/modules/evaluations/evaluations.service.ts +++ b/apps/gateway/src/modules/evaluations/evaluations.service.ts @@ -4,8 +4,10 @@ import { lastValueFrom } from 'rxjs'; import { EVALUATION_SERVICE_NAME, CRITERIONS_SERVICE_NAME, + TIE_BREAK_SERVICE_NAME, EvaluationServiceClient, CriterionsServiceClient, + TieBreakServiceClient, FindEvaluationsByEvaluatorResponse, FindCriterionsByCourseResponse, GetProjectStatsRequest, @@ -25,11 +27,13 @@ import { export class EvaluationsService implements OnModuleInit { private evaluationService: EvaluationServiceClient; private criterionsService: CriterionsServiceClient; + private tieBreakService: TieBreakServiceClient; private projectsService: ProjectsServiceClient; constructor( @Inject(EVALUATION_SERVICE_NAME) private evaluationClient: ClientGrpc, @Inject(CRITERIONS_SERVICE_NAME) private criterionsClient: ClientGrpc, + @Inject(TIE_BREAK_SERVICE_NAME) private tieBreakClient: ClientGrpc, @Inject(PROJECTS_SERVICE_NAME) private projectsClient: ClientGrpc, ) { } @@ -42,6 +46,10 @@ export class EvaluationsService implements OnModuleInit { this.criterionsClient.getService( CRITERIONS_SERVICE_NAME, ); + this.tieBreakService = + this.tieBreakClient.getService( + TIE_BREAK_SERVICE_NAME, + ); this.projectsService = this.projectsClient.getService( PROJECTS_SERVICE_NAME, @@ -184,10 +192,25 @@ export class EvaluationsService implements OnModuleInit { return { items: [], courseId, eventId }; } - // 3. Sort by averageGrade descending and take top N - const topProjects = projectStats - .sort((a, b) => b.averageGrade - a.averageGrade) - .slice(0, limit); + // 3. Sort by averageGrade descending, applying tiebreak order if there are ties + const sorted = projectStats.sort((a, b) => b.averageGrade - a.averageGrade); + + const hasTies = sorted.some((p, i, arr) => i > 0 && arr[i - 1].averageGrade === p.averageGrade); + + if (hasTies) { + const tiebreaksResponse = await lastValueFrom( + this.tieBreakService.listTieBreaks({ eventId, categoryId: courseId }), + ); + const tiebreakMap = new Map( + tiebreaksResponse.tiebreaks.map(tb => [tb.projectId, tb.tiebreakOrder]), + ); + sorted.sort((a, b) => { + if (b.averageGrade !== a.averageGrade) return b.averageGrade - a.averageGrade; + return (tiebreakMap.get(a.projectId) ?? Infinity) - (tiebreakMap.get(b.projectId) ?? Infinity); + }); + } + + const topProjects = sorted.slice(0, limit); // 4. Fetch full project details for the top 5 const topProjectIds = topProjects.map(tp => tp.projectId); From 187c40b2fec398e3e0558dcabbbec0ba195acace Mon Sep 17 00:00:00 2001 From: Wilmer Junior Santiago Donado Date: Thu, 28 May 2026 17:36:44 -0500 Subject: [PATCH 3/7] [CU-86e1kwwx0]: Return Tiebreak On GET /evaluations/courses/id/top-projects return the Tiebreak if exists anyone that can change the current order of the TOP --- .../src/modules/evaluations/evaluations.service.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/gateway/src/modules/evaluations/evaluations.service.ts b/apps/gateway/src/modules/evaluations/evaluations.service.ts index ada77b8e..effa8f2f 100644 --- a/apps/gateway/src/modules/evaluations/evaluations.service.ts +++ b/apps/gateway/src/modules/evaluations/evaluations.service.ts @@ -197,12 +197,15 @@ export class EvaluationsService implements OnModuleInit { const hasTies = sorted.some((p, i, arr) => i > 0 && arr[i - 1].averageGrade === p.averageGrade); + let appliedTiebreaks: import('@app/common/generated/evaluation').TieBreakProto[] = []; + if (hasTies) { const tiebreaksResponse = await lastValueFrom( this.tieBreakService.listTieBreaks({ eventId, categoryId: courseId }), ); + appliedTiebreaks = tiebreaksResponse.tiebreaks; const tiebreakMap = new Map( - tiebreaksResponse.tiebreaks.map(tb => [tb.projectId, tb.tiebreakOrder]), + appliedTiebreaks.map(tb => [tb.projectId, tb.tiebreakOrder]), ); sorted.sort((a, b) => { if (b.averageGrade !== a.averageGrade) return b.averageGrade - a.averageGrade; @@ -253,10 +256,14 @@ export class EvaluationsService implements OnModuleInit { }) .filter(p => p !== null); + const enrichedProjectIds = new Set(enrichedProjects.map(p => p.id)); + const relevantTiebreaks = appliedTiebreaks.filter(tb => enrichedProjectIds.has(tb.projectId)); + return { items: enrichedProjects, courseId, eventId, + ...(relevantTiebreaks.length > 0 && { tiebreaks: relevantTiebreaks }), }; } } From e1c940a887c182c13945563bbfc091f53c6e4b3d Mon Sep 17 00:00:00 2001 From: Wilmer Junior Santiago Donado Date: Thu, 28 May 2026 17:39:28 -0500 Subject: [PATCH 4/7] [CU-86e1kwwx0]: Return Tiebreak v.2 On GET /evaluations/courses/id/top-projects return the Tiebreak if exists anyone that can change the current order of the TOP --- apps/gateway/src/modules/evaluations/evaluations.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/gateway/src/modules/evaluations/evaluations.service.ts b/apps/gateway/src/modules/evaluations/evaluations.service.ts index effa8f2f..13cc34dc 100644 --- a/apps/gateway/src/modules/evaluations/evaluations.service.ts +++ b/apps/gateway/src/modules/evaluations/evaluations.service.ts @@ -8,6 +8,7 @@ import { EvaluationServiceClient, CriterionsServiceClient, TieBreakServiceClient, + TieBreakProto, FindEvaluationsByEvaluatorResponse, FindCriterionsByCourseResponse, GetProjectStatsRequest, @@ -197,7 +198,7 @@ export class EvaluationsService implements OnModuleInit { const hasTies = sorted.some((p, i, arr) => i > 0 && arr[i - 1].averageGrade === p.averageGrade); - let appliedTiebreaks: import('@app/common/generated/evaluation').TieBreakProto[] = []; + let appliedTiebreaks: TieBreakProto[] = []; if (hasTies) { const tiebreaksResponse = await lastValueFrom( From 9fc620d271515bfbf98ca8f351dd150c2c6df48c Mon Sep 17 00:00:00 2001 From: Wilmer Junior Santiago Donado Date: Thu, 28 May 2026 17:52:11 -0500 Subject: [PATCH 5/7] [CU-86e1kwwx0]: Return disputedProjects On GET /evaluations/courses/id/top-projects return all disputed project on the next m positions that are fighting for a position in the top --- .../evaluations/evaluations.service.ts | 79 +++++++++++-------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/apps/gateway/src/modules/evaluations/evaluations.service.ts b/apps/gateway/src/modules/evaluations/evaluations.service.ts index 13cc34dc..45346f17 100644 --- a/apps/gateway/src/modules/evaluations/evaluations.service.ts +++ b/apps/gateway/src/modules/evaluations/evaluations.service.ts @@ -216,45 +216,53 @@ export class EvaluationsService implements OnModuleInit { const topProjects = sorted.slice(0, limit); - // 4. Fetch full project details for the top 5 - const topProjectIds = topProjects.map(tp => tp.projectId); - const projectsPromises = topProjectIds.map(id => - lastValueFrom(this.projectsService.getProjectComplete({ id })) - .catch(() => null) // Handle deleted projects gracefully - ); + // Detect disputed: projects outside the limit tied with the last position + const disputedStats = topProjects.length > 0 + ? sorted.slice(limit).filter(p => p.averageGrade === topProjects[topProjects.length - 1].averageGrade) + : []; - const projectsResponses = await Promise.all(projectsPromises); + // 4. Fetch full project details for top N and disputed in parallel + const topProjectIds = topProjects.map(tp => tp.projectId); + const disputedProjectIds = disputedStats.map(dp => dp.projectId); + + const [projectsResponses, disputedResponses] = await Promise.all([ + Promise.all(topProjectIds.map(id => + lastValueFrom(this.projectsService.getProjectComplete({ id })).catch(() => null) + )), + Promise.all(disputedProjectIds.map(id => + lastValueFrom(this.projectsService.getProjectComplete({ id })).catch(() => null) + )), + ]); + + const buildEnrichedProject = (tp: { projectId: number; averageGrade: number; evaluationCount: number }, response: any) => { + if (!response || !response.items || response.items.length === 0) return null; + const project = response.items[0]; + return { + id: project.id, + eventId: project.eventId, + name: project.name, + description: project.description, + eventNumber: project.eventNumber, + createdAt: project.createdAt, + updatedAt: project.updatedAt, + courseId: project.courseId, + state: project.state, + reason: project.reason, + participants: project.participants, + documents: project.documents, + pendingParticipants: project.pendingParticipants, + averageGrade: tp.averageGrade, + evaluationCount: tp.evaluationCount, + }; + }; // 5. Merge data const enrichedProjects = topProjects - .map((tp, idx) => { - const projectResponse = projectsResponses[idx]; - - // Skip if project is deleted or response is null - if (!projectResponse || !projectResponse.items || projectResponse.items.length === 0) { - return null; - } - - const project = projectResponse.items[0]; - - return { - id: project.id, - eventId: project.eventId, - name: project.name, - description: project.description, - eventNumber: project.eventNumber, - createdAt: project.createdAt, - updatedAt: project.updatedAt, - courseId: project.courseId, - state: project.state, - reason: project.reason, - participants: project.participants, - documents: project.documents, - pendingParticipants: project.pendingParticipants, - averageGrade: tp.averageGrade, - evaluationCount: tp.evaluationCount, - }; - }) + .map((tp, idx) => buildEnrichedProject(tp, projectsResponses[idx])) + .filter(p => p !== null); + + const disputedProjects = disputedStats + .map((dp, idx) => buildEnrichedProject(dp, disputedResponses[idx])) .filter(p => p !== null); const enrichedProjectIds = new Set(enrichedProjects.map(p => p.id)); @@ -265,6 +273,7 @@ export class EvaluationsService implements OnModuleInit { courseId, eventId, ...(relevantTiebreaks.length > 0 && { tiebreaks: relevantTiebreaks }), + ...(disputedProjects.length > 0 && { disputedProjects }), }; } } From 1b1c1ba04ba73f44ec0678cdeda83b8a94b9c2d9 Mon Sep 17 00:00:00 2001 From: Wilmer Junior Santiago Donado Date: Thu, 28 May 2026 17:56:39 -0500 Subject: [PATCH 6/7] [CU-86e1kwwx0]: Return disputedProjects v.2 On GET /evaluations/courses/id/top-projects return all disputed project on the next m positions that are fighting for a position in the top --- .../src/modules/evaluations/evaluations.service.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/gateway/src/modules/evaluations/evaluations.service.ts b/apps/gateway/src/modules/evaluations/evaluations.service.ts index 45346f17..957f3f7d 100644 --- a/apps/gateway/src/modules/evaluations/evaluations.service.ts +++ b/apps/gateway/src/modules/evaluations/evaluations.service.ts @@ -214,14 +214,19 @@ export class EvaluationsService implements OnModuleInit { }); } + const tiebreakProjectIds = new Set(appliedTiebreaks.map(tb => tb.projectId)); + const topProjects = sorted.slice(0, limit); - // Detect disputed: projects outside the limit tied with the last position + // Detect disputed: projects outside the limit tied with the last position and without a tiebreak record const disputedStats = topProjects.length > 0 - ? sorted.slice(limit).filter(p => p.averageGrade === topProjects[topProjects.length - 1].averageGrade) + ? sorted.slice(limit).filter(p => + p.averageGrade === topProjects[topProjects.length - 1].averageGrade && + !tiebreakProjectIds.has(p.projectId) + ) : []; - // 4. Fetch full project details for top N and disputed in parallel + // 4. Fetch full project details for the top N and disputed projects in parallel const topProjectIds = topProjects.map(tp => tp.projectId); const disputedProjectIds = disputedStats.map(dp => dp.projectId); From 82643552641ba4d14adb8eca6b8b68c0b3e18c74 Mon Sep 17 00:00:00 2001 From: edadul Date: Thu, 28 May 2026 21:50:26 -0500 Subject: [PATCH 7/7] [CU-86e1m00n9]: removed confirmed jurors validation --- .../fetch-confirmed-juror-members.use-case.ts | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/apps/gateway/src/modules/events/use-cases/fetch-confirmed-juror-members.use-case.ts b/apps/gateway/src/modules/events/use-cases/fetch-confirmed-juror-members.use-case.ts index bfc2574c..6de48031 100644 --- a/apps/gateway/src/modules/events/use-cases/fetch-confirmed-juror-members.use-case.ts +++ b/apps/gateway/src/modules/events/use-cases/fetch-confirmed-juror-members.use-case.ts @@ -16,9 +16,6 @@ import { InvitationServiceClient, InvitationStatus, } from '@app/common/generated/invitation'; - -import { Logger } from '@nestjs/common'; - export type EventMember = NonNullable< ListEventMembersResponse['members'] >[number]; @@ -28,8 +25,6 @@ export class FetchConfirmedJurorMembersUseCase { private eventService: EventServiceClient; private authService: AuthServiceClient; private invitationService: InvitationServiceClient; - private logger = new Logger(FetchConfirmedJurorMembersUseCase.name); - constructor( @Inject(EVENT_SERVICE_NAME) private readonly eventClient: ClientGrpc, @Inject(AUTH_SERVICE_NAME) private readonly authClient: ClientGrpc, @@ -46,28 +41,39 @@ export class FetchConfirmedJurorMembersUseCase { ); } + // NOTE: The original implementation filtered jury members based on accepted invitations. + // I'm making changes just 10 hours before the event while studying for my final networking exam. + // The jury members haven't confirmed yet, and we need to assign them to the projects. + // Pure joy :) async execute(eventId: number): Promise { - const [members, acceptedInvitationUserIds] = await Promise.all([ - this.fetchAllEventMembers(eventId), - this.fetchAcceptedInvitationUserIds(eventId), - ]); - - if ( - !members || - members.length === 0 || - acceptedInvitationUserIds.size === 0 - ) { + // const [members, acceptedInvitationUserIds] = await Promise.all([ + // this.fetchAllEventMembers(eventId), + // this.fetchAcceptedInvitationUserIds(eventId), + // ]); + const members = await this.fetchAllEventMembers(eventId); + + // if ( + // !members || + // members.length === 0 || + // acceptedInvitationUserIds.size === 0 + // ) { + // return []; + // } + if (!members || members.length === 0) { return []; } const uniqueRoleIds = [...new Set(members.map((m) => m.roleId))]; const jurorRoleIds = await this.resolveJurorRoleIds(uniqueRoleIds); + // const result = members.filter( + // (member) => + // member.active && + // jurorRoleIds.has(member.roleId) && + // acceptedInvitationUserIds.has(member.userId), + // ); const result = members.filter( - (member) => - member.active && - jurorRoleIds.has(member.roleId) && - acceptedInvitationUserIds.has(member.userId), + (member) => member.active && jurorRoleIds.has(member.roleId), ); return result;