9
9
Param ,
10
10
Query ,
11
11
NotFoundException ,
12
+ BadRequestException ,
12
13
InternalServerErrorException ,
13
14
} from '@nestjs/common' ;
14
15
import {
@@ -31,6 +32,7 @@ import {
31
32
ReviewResponseDto ,
32
33
ReviewItemRequestDto ,
33
34
ReviewItemResponseDto ,
35
+ ReviewProgressResponseDto ,
34
36
mapReviewRequestToDto ,
35
37
mapReviewItemRequestToDto ,
36
38
} from 'src/dto/review.dto' ;
@@ -39,6 +41,7 @@ import { ScorecardStatus } from '../../dto/scorecard.dto';
39
41
import { LoggerService } from '../../shared/modules/global/logger.service' ;
40
42
import { PaginatedResponse , PaginationDto } from '../../dto/pagination.dto' ;
41
43
import { PrismaErrorService } from '../../shared/modules/global/prisma-error.service' ;
44
+ import { ResourceApiService } from '../../shared/modules/global/resource.service' ;
42
45
43
46
@ApiTags ( 'Reviews' )
44
47
@ApiBearerAuth ( )
@@ -49,6 +52,7 @@ export class ReviewController {
49
52
constructor (
50
53
private readonly prisma : PrismaService ,
51
54
private readonly prismaErrorService : PrismaErrorService ,
55
+ private readonly resourceApiService : ResourceApiService ,
52
56
) {
53
57
this . logger = LoggerService . forRoot ( 'ReviewController' ) ;
54
58
}
@@ -603,4 +607,171 @@ export class ReviewController {
603
607
} ) ;
604
608
}
605
609
}
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
+ }
606
777
}
0 commit comments