99 Param ,
1010 Query ,
1111 NotFoundException ,
12+ BadRequestException ,
1213 InternalServerErrorException ,
1314} from '@nestjs/common' ;
1415import {
@@ -31,6 +32,7 @@ import {
3132 ReviewResponseDto ,
3233 ReviewItemRequestDto ,
3334 ReviewItemResponseDto ,
35+ ReviewProgressResponseDto ,
3436 mapReviewRequestToDto ,
3537 mapReviewItemRequestToDto ,
3638} from 'src/dto/review.dto' ;
@@ -39,6 +41,7 @@ import { ScorecardStatus } from '../../dto/scorecard.dto';
3941import { LoggerService } from '../../shared/modules/global/logger.service' ;
4042import { PaginatedResponse , PaginationDto } from '../../dto/pagination.dto' ;
4143import { PrismaErrorService } from '../../shared/modules/global/prisma-error.service' ;
44+ import { ResourceApiService } from '../../shared/modules/global/resource.service' ;
4245
4346@ApiTags ( 'Reviews' )
4447@ApiBearerAuth ( )
@@ -49,6 +52,7 @@ export class ReviewController {
4952 constructor (
5053 private readonly prisma : PrismaService ,
5154 private readonly prismaErrorService : PrismaErrorService ,
55+ private readonly resourceApiService : ResourceApiService ,
5256 ) {
5357 this . logger = LoggerService . forRoot ( 'ReviewController' ) ;
5458 }
@@ -603,4 +607,171 @@ export class ReviewController {
603607 } ) ;
604608 }
605609 }
610+
611+ @Get ( '/progress/:challengeId' )
612+ @Roles ( UserRole . Admin , UserRole . Copilot , UserRole . Reviewer , UserRole . User )
613+ @Scopes ( Scope . ReadReview )
614+ @ApiOperation ( {
615+ summary : 'Get review progress for a specific challenge' ,
616+ description :
617+ 'Calculate and return the review progress percentage for a challenge. Accessible to all authenticated users. | Scopes: read:review' ,
618+ } )
619+ @ApiParam ( {
620+ name : 'challengeId' ,
621+ description : 'The ID of the challenge to calculate progress for' ,
622+ example : 'challenge123' ,
623+ } )
624+ @ApiResponse ( {
625+ status : 200 ,
626+ description : 'Review progress calculated successfully.' ,
627+ type : ReviewProgressResponseDto ,
628+ } )
629+ @ApiResponse ( {
630+ status : 400 ,
631+ description : 'Invalid challengeId parameter.' ,
632+ } )
633+ @ApiResponse ( {
634+ status : 404 ,
635+ description : 'Challenge not found or no data available.' ,
636+ } )
637+ @ApiResponse ( {
638+ status : 500 ,
639+ description : 'Server error during calculation.' ,
640+ } )
641+ async getReviewProgress (
642+ @Param ( 'challengeId' ) challengeId : string ,
643+ ) : Promise < ReviewProgressResponseDto > {
644+ this . logger . log (
645+ `Calculating review progress for challenge: ${ challengeId } ` ,
646+ ) ;
647+
648+ try {
649+ // Validate challengeId parameter
650+ if (
651+ ! challengeId ||
652+ typeof challengeId !== 'string' ||
653+ challengeId . trim ( ) === ''
654+ ) {
655+ throw new Error ( 'Invalid challengeId parameter' ) ;
656+ }
657+
658+ // Get reviewers from Resource API
659+ this . logger . debug ( 'Fetching reviewers from Resource API' ) ;
660+ const resources = await this . resourceApiService . getResources ( {
661+ challengeId,
662+ } ) ;
663+
664+ // Get resource roles to filter by reviewer role
665+ const resourceRoles = await this . resourceApiService . getResourceRoles ( ) ;
666+
667+ // Filter resources to get only reviewers
668+ const reviewers = resources . filter ( ( resource ) => {
669+ const role = resourceRoles [ resource . roleId ] ;
670+ return role && role . name . toLowerCase ( ) . includes ( 'reviewer' ) ;
671+ } ) ;
672+
673+ const totalReviewers = reviewers . length ;
674+ this . logger . debug (
675+ `Found ${ totalReviewers } reviewers for challenge ${ challengeId } ` ,
676+ ) ;
677+
678+ // Get submissions for the challenge
679+ this . logger . debug ( 'Fetching submissions for the challenge' ) ;
680+ const submissions = await this . prisma . submission . findMany ( {
681+ where : {
682+ challengeId,
683+ status : 'ACTIVE' ,
684+ } ,
685+ } ) ;
686+
687+ const submissionIds = submissions . map ( ( s ) => s . id ) ;
688+ const totalSubmissions = submissions . length ;
689+ this . logger . debug (
690+ `Found ${ totalSubmissions } submissions for challenge ${ challengeId } ` ,
691+ ) ;
692+
693+ // Get submitted reviews for these submissions
694+ this . logger . debug ( 'Fetching submitted reviews' ) ;
695+ const submittedReviews = await this . prisma . review . findMany ( {
696+ where : {
697+ submissionId : { in : submissionIds } ,
698+ committed : true ,
699+ } ,
700+ include : {
701+ reviewItems : true ,
702+ } ,
703+ } ) ;
704+
705+ const totalSubmittedReviews = submittedReviews . length ;
706+ this . logger . debug ( `Found ${ totalSubmittedReviews } submitted reviews` ) ;
707+
708+ // Calculate progress percentage
709+ let progressPercentage = 0 ;
710+
711+ if ( totalReviewers > 0 && totalSubmissions > 0 ) {
712+ const expectedTotalReviews = totalSubmissions * totalReviewers ;
713+ progressPercentage =
714+ ( totalSubmittedReviews / expectedTotalReviews ) * 100 ;
715+ // Round to 2 decimal places
716+ progressPercentage = Math . round ( progressPercentage * 100 ) / 100 ;
717+ }
718+
719+ // Handle edge cases
720+ if ( progressPercentage > 100 ) {
721+ progressPercentage = 100 ;
722+ }
723+
724+ const result : ReviewProgressResponseDto = {
725+ challengeId,
726+ totalReviewers,
727+ totalSubmissions,
728+ totalSubmittedReviews,
729+ progressPercentage,
730+ calculatedAt : new Date ( ) . toISOString ( ) ,
731+ } ;
732+
733+ this . logger . log (
734+ `Review progress calculated: ${ progressPercentage } % for challenge ${ challengeId } ` ,
735+ ) ;
736+ return result ;
737+ } catch ( error ) {
738+ this . logger . error (
739+ `Error calculating review progress for challenge ${ challengeId } :` ,
740+ error ,
741+ ) ;
742+
743+ if ( error . message === 'Invalid challengeId parameter' ) {
744+ throw new Error ( 'Invalid challengeId parameter' ) ;
745+ }
746+
747+ // Handle Resource API errors based on HTTP status codes
748+ if ( error . message === 'Cannot get data from Resource API.' ) {
749+ const statusCode = ( error as Error & { statusCode ?: number } )
750+ . statusCode ;
751+ if ( statusCode === 400 ) {
752+ throw new BadRequestException ( {
753+ message : `Challenge ID ${ challengeId } is not in valid GUID format` ,
754+ code : 'INVALID_CHALLENGE_ID' ,
755+ } ) ;
756+ } else if ( statusCode === 404 ) {
757+ throw new NotFoundException ( {
758+ message : `Challenge with ID ${ challengeId } was not found` ,
759+ code : 'CHALLENGE_NOT_FOUND' ,
760+ } ) ;
761+ }
762+ }
763+
764+ if ( error . message && error . message . includes ( 'not found' ) ) {
765+ throw new NotFoundException ( {
766+ message : `Challenge with ID ${ challengeId } was not found or has no data available` ,
767+ code : 'CHALLENGE_NOT_FOUND' ,
768+ } ) ;
769+ }
770+
771+ throw new InternalServerErrorException ( {
772+ message : 'Failed to calculate review progress' ,
773+ code : 'PROGRESS_CALCULATION_ERROR' ,
774+ } ) ;
775+ }
776+ }
606777}
0 commit comments