From f6202b250d87e2035beef6711621493e1dd8ce6c Mon Sep 17 00:00:00 2001 From: Adriana Zencke Zimmermann Date: Sat, 4 Apr 2026 14:42:01 +0200 Subject: [PATCH 1/5] feat: Add admin mentee review endpoints with approve/reject by mentee ID Introduce three new admin endpoints under MENTEE_APPROVE permission: GET /mentees/applications/review returns enriched priority-1 PENDING applications for admin review; PATCH /mentees/{id}/approve promotes the priority-1 application to MENTOR_REVIEWING; PATCH /mentees/{id}/reject rejects all PENDING applications for a mentee. Adds repository query methods findByStatusAndPriorityOrder and findPendingByMenteeId backed by new SQL queries, plus MenteeApplicationReviewDto combining application and mentee profile data. Unit and integration tests included. --- .../MenteeApplicationController.java | 56 ++++ .../MenteeApplicationReviewDto.java | 24 ++ .../MenteeApplicationRepository.java | 18 ++ .../PostgresMenteeApplicationRepository.java | 23 ++ .../service/MenteeWorkflowService.java | 121 ++++++++ .../service/MenteeWorkflowServiceTest.java | 261 ++++++++++++++---- ...onRepositoryNewMethodsIntegrationTest.java | 220 +++++++++++++++ 7 files changed, 672 insertions(+), 51 deletions(-) create mode 100644 src/main/java/com/wcc/platform/domain/platform/mentorship/MenteeApplicationReviewDto.java create mode 100644 src/testInt/java/com/wcc/platform/repository/postgres/PostgresMenteeApplicationRepositoryNewMethodsIntegrationTest.java diff --git a/src/main/java/com/wcc/platform/controller/MenteeApplicationController.java b/src/main/java/com/wcc/platform/controller/MenteeApplicationController.java index e8dca3f9..a054cffc 100644 --- a/src/main/java/com/wcc/platform/controller/MenteeApplicationController.java +++ b/src/main/java/com/wcc/platform/controller/MenteeApplicationController.java @@ -9,6 +9,7 @@ import com.wcc.platform.domain.platform.mentorship.ApplicationStatus; import com.wcc.platform.domain.platform.mentorship.ApplicationWithdrawRequest; import com.wcc.platform.domain.platform.mentorship.MenteeApplication; +import com.wcc.platform.domain.platform.mentorship.MenteeApplicationReviewDto; import com.wcc.platform.service.MenteeWorkflowService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -42,10 +43,65 @@ + "and the mentee itself.") @AllArgsConstructor @Validated +@SuppressWarnings({"PMD.ExcessiveImports"}) public class MenteeApplicationController { private final MenteeWorkflowService applicationService; + /** + * API for admin to retrieve all pending priority-1 mentee applications with enriched mentee + * profile data for review. + * + * @return list of review DTOs containing application and mentee profile details + */ + @GetMapping("/mentees/applications/review") + @RequiresPermission(Permission.MENTEE_APPROVE) + @Operation( + summary = "Get pending priority-1 mentee applications for admin review", + security = {@SecurityRequirement(name = "apiKey"), @SecurityRequirement(name = "bearerAuth")}) + @ResponseStatus(HttpStatus.OK) + public ResponseEntity> getPendingPriorityOneReviews() { + return ResponseEntity.ok(applicationService.getPendingPriorityOneReviews()); + } + + /** + * API for admin to approve a mentee by mentee ID. Only the priority-1 PENDING application is + * approved; all other PENDING applications remain unchanged (waiting). + * + * @param menteeId The mentee ID + * @return the approved application + */ + @PatchMapping("/mentees/{menteeId}/approve") + @RequiresPermission(Permission.MENTEE_APPROVE) + @Operation( + summary = "Admin approves mentee priority-1 application by mentee ID", + security = {@SecurityRequirement(name = "apiKey"), @SecurityRequirement(name = "bearerAuth")}) + @ResponseStatus(HttpStatus.OK) + public ResponseEntity approveMenteeByMenteeId( + @Parameter(description = "Mentee ID") @PathVariable final Long menteeId) { + return ResponseEntity.ok(applicationService.approveMenteeByMenteeId(menteeId)); + } + + /** + * API for admin to reject all PENDING applications of a mentee by mentee ID. + * + * @param menteeId The mentee ID + * @param request Rejection request containing the reason + * @return list of all rejected applications + */ + @PatchMapping("/mentees/{menteeId}/reject") + @RequiresPermission(Permission.MENTEE_APPROVE) + @Operation( + summary = "Admin rejects all pending applications for a mentee by mentee ID", + security = {@SecurityRequirement(name = "apiKey"), @SecurityRequirement(name = "bearerAuth")}) + @ResponseStatus(HttpStatus.OK) + public ResponseEntity> rejectMenteeByMenteeId( + @Parameter(description = "Mentee ID") @PathVariable final Long menteeId, + @Valid @RequestBody final ApplicationRejectRequest request) { + return ResponseEntity.ok( + applicationService.rejectMenteeByMenteeId(menteeId, request.reason())); + } + /** * API to get all applications submitted by a mentee for a specific cycle. * diff --git a/src/main/java/com/wcc/platform/domain/platform/mentorship/MenteeApplicationReviewDto.java b/src/main/java/com/wcc/platform/domain/platform/mentorship/MenteeApplicationReviewDto.java new file mode 100644 index 00000000..3e0fe8a5 --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/platform/mentorship/MenteeApplicationReviewDto.java @@ -0,0 +1,24 @@ +package com.wcc.platform.domain.platform.mentorship; + +/** + * DTO for admin review of a pending mentee application. + * + *

Combines application details with mentee profile information to support the admin review + * workflow for priority-1 PENDING applications. + * + * @param applicationId application ID + * @param menteeId mentee ID + * @param fullName mentee full name + * @param position current job position + * @param yearsExperience years of experience (may be null) + */ +public record MenteeApplicationReviewDto( + Long applicationId, + Long menteeId, + String fullName, + String position, + Integer yearsExperience, + String linkedinUrl, + String slackDisplayName, + String email, + String mentorshipGoal) {} diff --git a/src/main/java/com/wcc/platform/repository/MenteeApplicationRepository.java b/src/main/java/com/wcc/platform/repository/MenteeApplicationRepository.java index be5f0200..18b88462 100644 --- a/src/main/java/com/wcc/platform/repository/MenteeApplicationRepository.java +++ b/src/main/java/com/wcc/platform/repository/MenteeApplicationRepository.java @@ -81,4 +81,22 @@ public interface MenteeApplicationRepository extends CrudRepository findByStatusAndPriorityOrder( + ApplicationStatus status, Integer priorityOrder); + + /** + * Find all PENDING applications for a specific mentee across all cycles, ordered by priority. + * + * @param menteeId the mentee ID + * @return list of PENDING applications for the mentee ordered by priority + */ + List findPendingByMenteeId(Long menteeId); } diff --git a/src/main/java/com/wcc/platform/repository/postgres/mentorship/PostgresMenteeApplicationRepository.java b/src/main/java/com/wcc/platform/repository/postgres/mentorship/PostgresMenteeApplicationRepository.java index 8cd4c59c..41229e98 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/mentorship/PostgresMenteeApplicationRepository.java +++ b/src/main/java/com/wcc/platform/repository/postgres/mentorship/PostgresMenteeApplicationRepository.java @@ -43,6 +43,14 @@ public class PostgresMenteeApplicationRepository implements MenteeApplicationRep "SELECT * FROM mentee_applications WHERE application_status = ?::application_status " + "ORDER BY applied_at DESC"; + private static final String SEL_BY_STATUS_PRIO = + "SELECT * FROM mentee_applications WHERE application_status = ?::application_status " + + "AND priority_order = ? ORDER BY applied_at DESC"; + + private static final String SEL_PENDING_MENTEE = + "SELECT * FROM mentee_applications WHERE mentee_id = ? " + + "AND application_status = 'pending' ORDER BY priority_order"; + private static final String SEL_BY_MENTOR = "SELECT * FROM mentee_applications " + "WHERE mentee_id = ? AND mentor_id = ? AND cycle_id = ?"; @@ -167,6 +175,21 @@ public Long countMenteeApplications(final Long menteeId, final Long cycleId) { return count != null ? count : 0L; } + @Override + public List findByStatusAndPriorityOrder( + final ApplicationStatus status, final Integer priorityOrder) { + return jdbc.query( + SEL_BY_STATUS_PRIO, + (rs, rowNum) -> mapRow(rs), + status.getValue(), + priorityOrder); + } + + @Override + public List findPendingByMenteeId(final Long menteeId) { + return jdbc.query(SEL_PENDING_MENTEE, (rs, rowNum) -> mapRow(rs), menteeId); + } + private MenteeApplication mapRow(final ResultSet rs) throws SQLException { return MenteeApplication.builder() .applicationId(rs.getLong("application_id")) diff --git a/src/main/java/com/wcc/platform/service/MenteeWorkflowService.java b/src/main/java/com/wcc/platform/service/MenteeWorkflowService.java index 436ea9d7..adfc11be 100644 --- a/src/main/java/com/wcc/platform/service/MenteeWorkflowService.java +++ b/src/main/java/com/wcc/platform/service/MenteeWorkflowService.java @@ -4,10 +4,14 @@ import com.wcc.platform.domain.exceptions.ApplicationNotFoundException; import com.wcc.platform.domain.exceptions.ContentNotFoundException; import com.wcc.platform.domain.exceptions.MentorCapacityExceededException; +import com.wcc.platform.domain.platform.SocialNetwork; +import com.wcc.platform.domain.platform.SocialNetworkType; import com.wcc.platform.domain.platform.mentorship.ApplicationStatus; import com.wcc.platform.domain.platform.mentorship.MenteeApplication; +import com.wcc.platform.domain.platform.mentorship.MenteeApplicationReviewDto; import com.wcc.platform.domain.platform.mentorship.MentorshipCycleEntity; import com.wcc.platform.repository.MenteeApplicationRepository; +import com.wcc.platform.repository.MenteeRepository; import com.wcc.platform.repository.MentorshipCycleRepository; import com.wcc.platform.repository.MentorshipMatchRepository; import java.util.List; @@ -26,6 +30,7 @@ public class MenteeWorkflowService { private final MenteeApplicationRepository applicationRepository; + private final MenteeRepository menteeRepository; private final MentorshipMatchRepository matchRepository; private final MentorshipCycleRepository cycleRepository; @@ -199,6 +204,122 @@ public List getApplicationsByStatus(final ApplicationStatus s return applicationRepository.findByStatus(status); } + /** + * Returns all PENDING priority-1 mentee applications enriched with mentee profile data, for admin + * review. + * + * @return list of review DTOs containing application and mentee details + */ + public List getPendingPriorityOneReviews() { + return applicationRepository + .findByStatusAndPriorityOrder(ApplicationStatus.PENDING, 1) + .stream() + .map(this::toReviewDto) + .toList(); + } + + /** + * Admin approves a mentee by mentee ID. Only the priority-1 PENDING application is moved to + * MENTOR_REVIEWING; all other PENDING applications remain unchanged. + * + * @param menteeId the mentee ID + * @return the approved application + * @throws ContentNotFoundException if no priority-1 PENDING application exists for the mentee + */ + @Transactional + public MenteeApplication approveMenteeByMenteeId(final Long menteeId) { + final List pending = applicationRepository.findPendingByMenteeId(menteeId); + + final MenteeApplication priorityOne = + pending.stream() + .filter(app -> app.getPriorityOrder() == 1) + .findFirst() + .orElseThrow( + () -> + new ContentNotFoundException( + "No pending priority-1 application found for mentee " + menteeId)); + + final MenteeApplication updated = + applicationRepository.updateStatus( + priorityOne.getApplicationId(), ApplicationStatus.MENTOR_REVIEWING, null); + + log.info( + "Mentee {} priority-1 application {} approved and forwarded to mentor {}", + menteeId, + priorityOne.getApplicationId(), + priorityOne.getMentorId()); + + return updated; + } + + /** + * Admin rejects all PENDING applications for a mentee by mentee ID. + * + * @param menteeId the mentee ID + * @param reason the reason for rejection + * @return list of all rejected applications + * @throws ContentNotFoundException if no PENDING applications exist for the mentee + */ + @Transactional + public List rejectMenteeByMenteeId( + final Long menteeId, final String reason) { + final List pending = applicationRepository.findPendingByMenteeId(menteeId); + + if (pending.isEmpty()) { + throw new ContentNotFoundException( + "No pending applications found for mentee " + menteeId); + } + + final List rejected = + pending.stream() + .map( + app -> + applicationRepository.updateStatus( + app.getApplicationId(), ApplicationStatus.REJECTED, reason)) + .toList(); + + log.info( + "All {} pending applications for mentee {} rejected by the Mentorship Team", + rejected.size(), + menteeId); + + return rejected; + } + + private MenteeApplicationReviewDto toReviewDto(final MenteeApplication application) { + return menteeRepository + .findById(application.getMenteeId()) + .map( + mentee -> { + final String linkedinUrl = + mentee.getNetwork() == null + ? null + : mentee.getNetwork().stream() + .filter(n -> n.type() == SocialNetworkType.LINKEDIN) + .findFirst() + .map(SocialNetwork::link) + .orElse(null); + + final Integer yearsExperience = + mentee.getSkills() != null ? mentee.getSkills().yearsExperience() : null; + + return new MenteeApplicationReviewDto( + application.getApplicationId(), + mentee.getId(), + mentee.getFullName(), + mentee.getPosition(), + yearsExperience, + linkedinUrl, + mentee.getSlackDisplayName(), + mentee.getEmail(), + application.getWhyMentor()); + }) + .orElseThrow( + () -> + new ContentNotFoundException( + "Mentee not found for application " + application.getApplicationId())); + } + private MenteeApplication getApplicationOrThrow(final Long applicationId) { return applicationRepository .findById(applicationId) diff --git a/src/test/java/com/wcc/platform/service/MenteeWorkflowServiceTest.java b/src/test/java/com/wcc/platform/service/MenteeWorkflowServiceTest.java index 0e3fb683..92b343a2 100644 --- a/src/test/java/com/wcc/platform/service/MenteeWorkflowServiceTest.java +++ b/src/test/java/com/wcc/platform/service/MenteeWorkflowServiceTest.java @@ -4,13 +4,21 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; +import com.wcc.platform.domain.cms.attributes.Country; import com.wcc.platform.domain.exceptions.ApplicationNotFoundException; import com.wcc.platform.domain.exceptions.ContentNotFoundException; +import com.wcc.platform.domain.platform.SocialNetwork; +import com.wcc.platform.domain.platform.SocialNetworkType; import com.wcc.platform.domain.platform.mentorship.ApplicationStatus; +import com.wcc.platform.domain.platform.mentorship.Mentee; import com.wcc.platform.domain.platform.mentorship.MenteeApplication; +import com.wcc.platform.domain.platform.mentorship.MenteeApplicationReviewDto; +import com.wcc.platform.domain.platform.mentorship.Skills; import com.wcc.platform.repository.MenteeApplicationRepository; +import com.wcc.platform.repository.MenteeRepository; import com.wcc.platform.repository.MentorshipCycleRepository; import com.wcc.platform.repository.MentorshipMatchRepository; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -21,9 +29,10 @@ class MenteeWorkflowServiceTest { private static final String REJECTION_REASON = - "Application does not meet the eligibility criteria"; + "Application does not meet the eligibility criteria for this mentorship cycle"; @Mock private MenteeApplicationRepository applicationRepository; + @Mock private MenteeRepository menteeRepository; @Mock private MentorshipMatchRepository matchRepository; @Mock private MentorshipCycleRepository cycleRepository; @@ -32,34 +41,17 @@ class MenteeWorkflowServiceTest { @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - service = new MenteeWorkflowService(applicationRepository, matchRepository, cycleRepository); + service = + new MenteeWorkflowService( + applicationRepository, menteeRepository, matchRepository, cycleRepository); } @Test @DisplayName( - "Given a PENDING application, when admin approves it, then status is updated to MENTOR_REVIEWING") + "Given a PENDING application, when admin approves, then status becomes MENTOR_REVIEWING") void shouldApprovePendingApplicationAndUpdateStatusToMentorReviewing() { - final MenteeApplication pending = - MenteeApplication.builder() - .applicationId(1L) - .menteeId(10L) - .mentorId(20L) - .cycleId(5L) - .priorityOrder(1) - .status(ApplicationStatus.PENDING) - .whyMentor("Great mentor") - .build(); - - final MenteeApplication approved = - MenteeApplication.builder() - .applicationId(1L) - .menteeId(10L) - .mentorId(20L) - .cycleId(5L) - .priorityOrder(1) - .status(ApplicationStatus.MENTOR_REVIEWING) - .whyMentor("Great mentor") - .build(); + final MenteeApplication pending = pendingApplication(1L, 10L, 1); + final MenteeApplication approved = reviewingApplication(1L, 10L); when(applicationRepository.findById(1L)).thenReturn(Optional.of(pending)); when(applicationRepository.updateStatus(1L, ApplicationStatus.MENTOR_REVIEWING, null)) @@ -72,7 +64,7 @@ void shouldApprovePendingApplicationAndUpdateStatusToMentorReviewing() { @Test @DisplayName( - "Given a non-PENDING application, when admin approves it, then ContentNotFoundException is thrown") + "Given non-PENDING application, when admin approves, then ContentNotFoundException is thrown") void shouldThrowContentNotFoundExceptionWhenApprovedApplicationIsNotPending() { final MenteeApplication reviewing = MenteeApplication.builder() @@ -94,7 +86,7 @@ void shouldThrowContentNotFoundExceptionWhenApprovedApplicationIsNotPending() { @Test @DisplayName( - "Given an application that does not exist, when admin approves it, then ApplicationNotFoundException is thrown") + "Given application not found, when admin approves, then ApplicationNotFoundException thrown") void shouldThrowApplicationNotFoundExceptionWhenApprovedApplicationDoesNotExist() { when(applicationRepository.findById(99L)).thenReturn(Optional.empty()); @@ -105,29 +97,10 @@ void shouldThrowApplicationNotFoundExceptionWhenApprovedApplicationDoesNotExist( @Test @DisplayName( - "Given a PENDING application, when admin rejects it, then status is updated to REJECTED") + "Given a PENDING application, when admin rejects, then status becomes REJECTED") void shouldRejectPendingApplicationAndUpdateStatusToRejected() { - final MenteeApplication pending = - MenteeApplication.builder() - .applicationId(1L) - .menteeId(10L) - .mentorId(20L) - .cycleId(5L) - .priorityOrder(1) - .status(ApplicationStatus.PENDING) - .whyMentor("Great mentor") - .build(); - - final MenteeApplication rejected = - MenteeApplication.builder() - .applicationId(1L) - .menteeId(10L) - .mentorId(20L) - .cycleId(5L) - .priorityOrder(1) - .status(ApplicationStatus.REJECTED) - .whyMentor("Great mentor") - .build(); + final MenteeApplication pending = pendingApplication(1L, 10L, 1); + final MenteeApplication rejected = rejectedApplication(1L, 10L); when(applicationRepository.findById(1L)).thenReturn(Optional.of(pending)); when(applicationRepository.updateStatus(1L, ApplicationStatus.REJECTED, REJECTION_REASON)) @@ -140,7 +113,7 @@ void shouldRejectPendingApplicationAndUpdateStatusToRejected() { @Test @DisplayName( - "Given a non-PENDING application, when admin rejects it, then ContentNotFoundException is thrown") + "Given non-PENDING application, when admin rejects, then ContentNotFoundException is thrown") void shouldThrowContentNotFoundExceptionWhenRejectedApplicationIsNotPending() { final MenteeApplication rejected = MenteeApplication.builder() @@ -162,7 +135,7 @@ void shouldThrowContentNotFoundExceptionWhenRejectedApplicationIsNotPending() { @Test @DisplayName( - "Given an application that does not exist, when admin rejects it, then ApplicationNotFoundException is thrown") + "Given application not found, when admin rejects, then ApplicationNotFoundException thrown") void shouldThrowApplicationNotFoundExceptionWhenApplicationDoesNotExist() { when(applicationRepository.findById(99L)).thenReturn(Optional.empty()); @@ -170,4 +143,190 @@ void shouldThrowApplicationNotFoundExceptionWhenApplicationDoesNotExist() { .isInstanceOf(ApplicationNotFoundException.class) .hasMessageContaining("Application not found with ID: 99"); } -} + + @Test + @DisplayName( + "Given PENDING priority-1 apps exist, when getting reviews, then enriched DTOs are returned") + void shouldReturnEnrichedDtosForPendingPriorityOneApplications() { + final MenteeApplication app = pendingApplication(1L, 10L, 1); + final Mentee mentee = menteeWithLinkedIn(); + + when(applicationRepository.findByStatusAndPriorityOrder(ApplicationStatus.PENDING, 1)) + .thenReturn(List.of(app)); + when(menteeRepository.findById(10L)).thenReturn(Optional.of(mentee)); + + final List result = service.getPendingPriorityOneReviews(); + + assertThat(result).hasSize(1); + final MenteeApplicationReviewDto dto = result.getFirst(); + assertThat(dto.applicationId()).isEqualTo(1L); + assertThat(dto.menteeId()).isEqualTo(10L); + assertThat(dto.fullName()).isEqualTo("Jane Doe"); + assertThat(dto.email()).isEqualTo("jane@wcc.com"); + assertThat(dto.mentorshipGoal()).isEqualTo("Great mentor"); + assertThat(dto.yearsExperience()).isEqualTo(3); + assertThat(dto.linkedinUrl()).isEqualTo("https://linkedin.com/jane"); + } + + @Test + @DisplayName( + "Given no PENDING priority-1 applications, when getting reviews, then empty list is returned") + void shouldReturnEmptyListWhenNoPendingPriorityOneApplications() { + when(applicationRepository.findByStatusAndPriorityOrder(ApplicationStatus.PENDING, 1)) + .thenReturn(List.of()); + + final List result = service.getPendingPriorityOneReviews(); + + assertThat(result).isEmpty(); + } + + @Test + @DisplayName( + "Given mentee not found for pending app, when getting reviews, then ContentNotFoundException") + void shouldThrowContentNotFoundExceptionWhenMenteeNotFoundForPendingApplication() { + final MenteeApplication app = pendingApplication(1L, 10L, 1); + + when(applicationRepository.findByStatusAndPriorityOrder(ApplicationStatus.PENDING, 1)) + .thenReturn(List.of(app)); + when(menteeRepository.findById(10L)).thenReturn(Optional.empty()); + + assertThatThrownBy(service::getPendingPriorityOneReviews) + .isInstanceOf(ContentNotFoundException.class) + .hasMessageContaining("Mentee not found for application 1"); + } + + @Test + @DisplayName( + "Given mentee has a PENDING priority-1 app, when approving by menteeId, then it is approved") + void shouldApprovePriorityOneApplicationWhenApprovingByMenteeId() { + final MenteeApplication priorityOne = pendingApplication(1L, 10L, 1); + final MenteeApplication priorityTwo = pendingApplication(2L, 10L, 2); + final MenteeApplication approved = reviewingApplication(1L, 10L); + + when(applicationRepository.findPendingByMenteeId(10L)) + .thenReturn(List.of(priorityOne, priorityTwo)); + when(applicationRepository.updateStatus(1L, ApplicationStatus.MENTOR_REVIEWING, null)) + .thenReturn(approved); + + final MenteeApplication result = service.approveMenteeByMenteeId(10L); + + assertThat(result.getStatus()).isEqualTo(ApplicationStatus.MENTOR_REVIEWING); + assertThat(result.getApplicationId()).isEqualTo(1L); + } + + @Test + @DisplayName( + "Given mentee has no PENDING applications, when approving by menteeId, " + + "then ContentNotFoundException is thrown") + void shouldThrowContentNotFoundExceptionWhenNoAppsFoundForMenteeApproval() { + when(applicationRepository.findPendingByMenteeId(10L)).thenReturn(List.of()); + + assertThatThrownBy(() -> service.approveMenteeByMenteeId(10L)) + .isInstanceOf(ContentNotFoundException.class) + .hasMessageContaining("No pending priority-1 application found for mentee 10"); + } + + @Test + @DisplayName( + "Given mentee has PENDING apps but none is priority-1, when approving by menteeId, " + + "then ContentNotFoundException is thrown") + void shouldThrowContentNotFoundExceptionWhenNoPriorityOneAppForMentee() { + final MenteeApplication priorityTwo = pendingApplication(2L, 10L, 2); + + when(applicationRepository.findPendingByMenteeId(10L)).thenReturn(List.of(priorityTwo)); + + assertThatThrownBy(() -> service.approveMenteeByMenteeId(10L)) + .isInstanceOf(ContentNotFoundException.class) + .hasMessageContaining("No pending priority-1 application found for mentee 10"); + } + + @Test + @DisplayName( + "Given mentee has multiple PENDING applications, when rejecting by menteeId, " + + "then all are rejected") + void shouldRejectAllPendingApplicationsWhenRejectingByMenteeId() { + final MenteeApplication app1 = pendingApplication(1L, 10L, 1); + final MenteeApplication app2 = pendingApplication(2L, 10L, 2); + final MenteeApplication rejected1 = rejectedApplication(1L, 10L); + final MenteeApplication rejected2 = rejectedApplication(2L, 10L); + + when(applicationRepository.findPendingByMenteeId(10L)).thenReturn(List.of(app1, app2)); + when(applicationRepository.updateStatus(1L, ApplicationStatus.REJECTED, REJECTION_REASON)) + .thenReturn(rejected1); + when(applicationRepository.updateStatus(2L, ApplicationStatus.REJECTED, REJECTION_REASON)) + .thenReturn(rejected2); + + final List result = service.rejectMenteeByMenteeId(10L, REJECTION_REASON); + + assertThat(result).hasSize(2); + assertThat(result).allMatch(app -> app.getStatus() == ApplicationStatus.REJECTED); + } + + @Test + @DisplayName( + "Given mentee has no PENDING applications, when rejecting by menteeId, " + + "then ContentNotFoundException is thrown") + void shouldThrowContentNotFoundExceptionWhenNoAppsFoundForMenteeRejection() { + when(applicationRepository.findPendingByMenteeId(10L)).thenReturn(List.of()); + + assertThatThrownBy(() -> service.rejectMenteeByMenteeId(10L, REJECTION_REASON)) + .isInstanceOf(ContentNotFoundException.class) + .hasMessageContaining("No pending applications found for mentee 10"); + } + + private MenteeApplication pendingApplication( + final Long applicationId, final Long menteeId, final int priority) { + return MenteeApplication.builder() + .applicationId(applicationId) + .menteeId(menteeId) + .mentorId(20L) + .cycleId(5L) + .priorityOrder(priority) + .status(ApplicationStatus.PENDING) + .whyMentor("Great mentor") + .build(); + } + + private MenteeApplication reviewingApplication( + final Long applicationId, final Long menteeId) { + return MenteeApplication.builder() + .applicationId(applicationId) + .menteeId(menteeId) + .mentorId(20L) + .cycleId(5L) + .priorityOrder(1) + .status(ApplicationStatus.MENTOR_REVIEWING) + .whyMentor("Great mentor") + .build(); + } + + private MenteeApplication rejectedApplication( + final Long applicationId, final Long menteeId) { + return MenteeApplication.builder() + .applicationId(applicationId) + .menteeId(menteeId) + .mentorId(20L) + .cycleId(5L) + .priorityOrder(1) + .status(ApplicationStatus.REJECTED) + .whyMentor("Great mentor") + .build(); + } + + private Mentee menteeWithLinkedIn() { + return Mentee.menteeBuilder() + .id(10L) + .fullName("Jane Doe") + .email("jane@wcc.com") + .position("Software Engineer") + .slackDisplayName("jane.doe") + .country(new Country("GB", "United Kingdom")) + .bio("A motivated mentee") + .skills(new Skills(3, List.of(), List.of(), List.of())) + .availableHsMonth(5) + .spokenLanguages(List.of("English")) + .network( + List.of(new SocialNetwork(SocialNetworkType.LINKEDIN, "https://linkedin.com/jane"))) + .build(); + } +} \ No newline at end of file diff --git a/src/testInt/java/com/wcc/platform/repository/postgres/PostgresMenteeApplicationRepositoryNewMethodsIntegrationTest.java b/src/testInt/java/com/wcc/platform/repository/postgres/PostgresMenteeApplicationRepositoryNewMethodsIntegrationTest.java new file mode 100644 index 00000000..4d7709af --- /dev/null +++ b/src/testInt/java/com/wcc/platform/repository/postgres/PostgresMenteeApplicationRepositoryNewMethodsIntegrationTest.java @@ -0,0 +1,220 @@ +package com.wcc.platform.repository.postgres; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.wcc.platform.domain.platform.mentorship.ApplicationStatus; +import com.wcc.platform.domain.platform.mentorship.CycleStatus; +import com.wcc.platform.domain.platform.mentorship.Mentee; +import com.wcc.platform.domain.platform.mentorship.MenteeApplication; +import com.wcc.platform.domain.platform.mentorship.Mentor; +import com.wcc.platform.domain.platform.mentorship.MentorshipCycleEntity; +import com.wcc.platform.domain.platform.mentorship.MentorshipType; +import com.wcc.platform.factories.SetupMenteeFactories; +import com.wcc.platform.factories.SetupMentorFactories; +import com.wcc.platform.repository.postgres.mentorship.PostgresMenteeApplicationRepository; +import com.wcc.platform.repository.postgres.mentorship.PostgresMenteeRepository; +import com.wcc.platform.repository.postgres.mentorship.PostgresMentorRepository; +import com.wcc.platform.repository.postgres.mentorship.PostgresMentorshipCycleRepository; +import java.time.LocalDate; +import java.time.Month; +import java.time.Year; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests for the new query methods added to PostgresMenteeApplicationRepository: + * findByStatusAndPriorityOrder and findPendingByMenteeId. + */ +class PostgresMenteeApplicationRepositoryNewMethodsIntegrationTest extends DefaultDatabaseSetup { + + @Autowired private PostgresMenteeApplicationRepository applicationRepository; + @Autowired private PostgresMenteeRepository menteeRepository; + @Autowired private PostgresMentorRepository mentorRepository; + @Autowired private PostgresMentorshipCycleRepository cycleRepository; + @Autowired private PostgresMemberRepository memberRepository; + + private Mentee mentee; + private Mentor mentorA; + private Mentor mentorB; + private MentorshipCycleEntity cycle; + + @BeforeEach + void setUp() { + memberRepository.deleteByEmail("mentor_new_a@test.com"); + memberRepository.deleteByEmail("mentor_new_b@test.com"); + memberRepository.deleteByEmail("mentee_new@test.com"); + cycleRepository + .findByYearAndType(Year.of(2027), MentorshipType.LONG_TERM) + .ifPresent(c -> cycleRepository.deleteById(c.getCycleId())); + + cycle = + cycleRepository.create( + MentorshipCycleEntity.builder() + .cycleYear(Year.of(2027)) + .mentorshipType(MentorshipType.LONG_TERM) + .cycleMonth(Month.MARCH) + .registrationStartDate(LocalDate.now().minusDays(1)) + .registrationEndDate(LocalDate.now().plusDays(10)) + .cycleStartDate(LocalDate.now().plusDays(15)) + .status(CycleStatus.OPEN) + .maxMenteesPerMentor(3) + .description("New Methods Test Cycle") + .build()); + + mentorA = + mentorRepository.create( + SetupMentorFactories.createMentorTest(null, "Mentor A", "mentor_new_a@test.com")); + + mentorB = + mentorRepository.create( + SetupMentorFactories.createMentorTest(null, "Mentor B", "mentor_new_b@test.com")); + + mentee = + menteeRepository.create( + SetupMenteeFactories.createMenteeTest(null, "Mentee New", "mentee_new@test.com")); + } + + @AfterEach + void tearDown() { + if (mentee != null) { + menteeRepository.deleteById(mentee.getId()); + memberRepository.deleteById(mentee.getId()); + } + if (mentorA != null) { + mentorRepository.deleteById(mentorA.getId()); + memberRepository.deleteById(mentorA.getId()); + } + if (mentorB != null) { + mentorRepository.deleteById(mentorB.getId()); + memberRepository.deleteById(mentorB.getId()); + } + if (cycle != null) { + cycleRepository.deleteById(cycle.getCycleId()); + } + } + + @Test + @DisplayName( + "Given PENDING priority-1 application, when finding by status and priority, then returned") + void shouldFindByStatusAndPriorityOrder() { + final MenteeApplication created = createApplication(mentorA.getId(), 1, ApplicationStatus.PENDING); + + final List result = + applicationRepository.findByStatusAndPriorityOrder(ApplicationStatus.PENDING, 1); + + assertThat(result) + .anyMatch(app -> app.getApplicationId().equals(created.getApplicationId())); + } + + @Test + @DisplayName( + "Given priority-2 app, when finding by status and priority-1, then it is not returned") + void shouldNotReturnWrongPriorityWhenFindingByStatusAndPriority() { + final MenteeApplication created = createApplication(mentorA.getId(), 2, ApplicationStatus.PENDING); + + final List result = + applicationRepository.findByStatusAndPriorityOrder(ApplicationStatus.PENDING, 1); + + assertThat(result) + .noneMatch(app -> app.getApplicationId().equals(created.getApplicationId())); + } + + @Test + @DisplayName( + "Given MENTOR_REVIEWING app, when finding PENDING by priority-1, then it is not returned") + void shouldNotReturnWrongStatusWhenFindingByStatusAndPriority() { + final MenteeApplication created = createApplication(mentorA.getId(), 1, ApplicationStatus.PENDING); + applicationRepository.updateStatus( + created.getApplicationId(), ApplicationStatus.MENTOR_REVIEWING, null); + + final List result = + applicationRepository.findByStatusAndPriorityOrder(ApplicationStatus.PENDING, 1); + + assertThat(result) + .noneMatch(app -> app.getApplicationId().equals(created.getApplicationId())); + } + + @Test + @DisplayName( + "Given mentee has two PENDING apps to different mentors, when finding pending by menteeId, " + + "then both are returned") + void shouldFindPendingByMenteeId() { + final MenteeApplication app1 = createApplication(mentorA.getId(), 1, ApplicationStatus.PENDING); + final MenteeApplication app2 = createApplication(mentorB.getId(), 2, ApplicationStatus.PENDING); + + final List result = + applicationRepository.findPendingByMenteeId(mentee.getId()); + + assertThat(result).hasSize(2); + assertThat(result) + .extracting(MenteeApplication::getApplicationId) + .containsExactlyInAnyOrder(app1.getApplicationId(), app2.getApplicationId()); + } + + @Test + @DisplayName( + "Given mentee has PENDING and REJECTED apps, when finding pending by menteeId, " + + "then only PENDING is returned") + void shouldReturnOnlyPendingWhenFindingPendingByMenteeId() { + final MenteeApplication pending = createApplication(mentorA.getId(), 1, ApplicationStatus.PENDING); + final MenteeApplication toReject = createApplication(mentorB.getId(), 2, ApplicationStatus.PENDING); + applicationRepository.updateStatus( + toReject.getApplicationId(), ApplicationStatus.REJECTED, "Does not qualify"); + + final List result = + applicationRepository.findPendingByMenteeId(mentee.getId()); + + assertThat(result).hasSize(1); + assertThat(result.getFirst().getApplicationId()).isEqualTo(pending.getApplicationId()); + } + + @Test + @DisplayName( + "Given mentee has no applications, when finding pending by menteeId, " + + "then empty list is returned") + void shouldReturnEmptyListWhenNoAppsForMentee() { + final List result = + applicationRepository.findPendingByMenteeId(mentee.getId()); + + assertThat(result).isEmpty(); + } + + @Test + @DisplayName( + "Given two PENDING apps for mentee, when finding pending by menteeId, " + + "then results are ordered by priority") + void shouldReturnAppsOrderedByPriorityWhenFindingPendingByMenteeId() { + createApplication(mentorB.getId(), 2, ApplicationStatus.PENDING); + createApplication(mentorA.getId(), 1, ApplicationStatus.PENDING); + + final List result = + applicationRepository.findPendingByMenteeId(mentee.getId()); + + assertThat(result).hasSize(2); + assertThat(result.get(0).getPriorityOrder()).isEqualTo(1); + assertThat(result.get(1).getPriorityOrder()).isEqualTo(2); + } + + private MenteeApplication createApplication( + final Long mentorId, final int priority, final ApplicationStatus status) { + final MenteeApplication app = + applicationRepository.create( + MenteeApplication.builder() + .menteeId(mentee.getId()) + .mentorId(mentorId) + .cycleId(cycle.getCycleId()) + .priorityOrder(priority) + .status(ApplicationStatus.PENDING) + .whyMentor("Because of their expertise") + .build()); + + if (status != ApplicationStatus.PENDING) { + return applicationRepository.updateStatus(app.getApplicationId(), status, "Status update"); + } + return app; + } +} From 70e0c04e082af5bdd88b4b11747ec285649d4ed7 Mon Sep 17 00:00:00 2001 From: Adriana Zencke Zimmermann Date: Sun, 5 Apr 2026 13:04:41 +0200 Subject: [PATCH 2/5] feat: Add admin mentee review endpoints with approve/reject by mentee ID --- .../MenteeApplicationController.java | 55 ------ .../platform/controller/MenteeController.java | 66 ++++++- .../platform/repository/MenteeRepository.java | 18 ++ .../mentorship/PostgresMenteeRepository.java | 18 ++ .../platform/service/MenteeAdminService.java | 78 +++++++++ .../wcc/platform/service/MenteeService.java | 12 +- .../service/MenteeWorkflowService.java | 121 ------------- .../controller/MenteeControllerTest.java | 129 +++++++++++++- .../service/MenteeAdminServiceTest.java | 122 +++++++++++++ .../platform/service/MenteeServiceTest.java | 13 +- .../service/MenteeWorkflowServiceTest.java | 163 +----------------- 11 files changed, 438 insertions(+), 357 deletions(-) create mode 100644 src/main/java/com/wcc/platform/service/MenteeAdminService.java create mode 100644 src/test/java/com/wcc/platform/service/MenteeAdminServiceTest.java diff --git a/src/main/java/com/wcc/platform/controller/MenteeApplicationController.java b/src/main/java/com/wcc/platform/controller/MenteeApplicationController.java index a054cffc..0c442a43 100644 --- a/src/main/java/com/wcc/platform/controller/MenteeApplicationController.java +++ b/src/main/java/com/wcc/platform/controller/MenteeApplicationController.java @@ -9,7 +9,6 @@ import com.wcc.platform.domain.platform.mentorship.ApplicationStatus; import com.wcc.platform.domain.platform.mentorship.ApplicationWithdrawRequest; import com.wcc.platform.domain.platform.mentorship.MenteeApplication; -import com.wcc.platform.domain.platform.mentorship.MenteeApplicationReviewDto; import com.wcc.platform.service.MenteeWorkflowService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -48,60 +47,6 @@ public class MenteeApplicationController { private final MenteeWorkflowService applicationService; - /** - * API for admin to retrieve all pending priority-1 mentee applications with enriched mentee - * profile data for review. - * - * @return list of review DTOs containing application and mentee profile details - */ - @GetMapping("/mentees/applications/review") - @RequiresPermission(Permission.MENTEE_APPROVE) - @Operation( - summary = "Get pending priority-1 mentee applications for admin review", - security = {@SecurityRequirement(name = "apiKey"), @SecurityRequirement(name = "bearerAuth")}) - @ResponseStatus(HttpStatus.OK) - public ResponseEntity> getPendingPriorityOneReviews() { - return ResponseEntity.ok(applicationService.getPendingPriorityOneReviews()); - } - - /** - * API for admin to approve a mentee by mentee ID. Only the priority-1 PENDING application is - * approved; all other PENDING applications remain unchanged (waiting). - * - * @param menteeId The mentee ID - * @return the approved application - */ - @PatchMapping("/mentees/{menteeId}/approve") - @RequiresPermission(Permission.MENTEE_APPROVE) - @Operation( - summary = "Admin approves mentee priority-1 application by mentee ID", - security = {@SecurityRequirement(name = "apiKey"), @SecurityRequirement(name = "bearerAuth")}) - @ResponseStatus(HttpStatus.OK) - public ResponseEntity approveMenteeByMenteeId( - @Parameter(description = "Mentee ID") @PathVariable final Long menteeId) { - return ResponseEntity.ok(applicationService.approveMenteeByMenteeId(menteeId)); - } - - /** - * API for admin to reject all PENDING applications of a mentee by mentee ID. - * - * @param menteeId The mentee ID - * @param request Rejection request containing the reason - * @return list of all rejected applications - */ - @PatchMapping("/mentees/{menteeId}/reject") - @RequiresPermission(Permission.MENTEE_APPROVE) - @Operation( - summary = "Admin rejects all pending applications for a mentee by mentee ID", - security = {@SecurityRequirement(name = "apiKey"), @SecurityRequirement(name = "bearerAuth")}) - @ResponseStatus(HttpStatus.OK) - public ResponseEntity> rejectMenteeByMenteeId( - @Parameter(description = "Mentee ID") @PathVariable final Long menteeId, - @Valid @RequestBody final ApplicationRejectRequest request) { - return ResponseEntity.ok( - applicationService.rejectMenteeByMenteeId(menteeId, request.reason())); - } - /** * API to get all applications submitted by a mentee for a specific cycle. * diff --git a/src/main/java/com/wcc/platform/controller/MenteeController.java b/src/main/java/com/wcc/platform/controller/MenteeController.java index 8f79c457..8243d48b 100644 --- a/src/main/java/com/wcc/platform/controller/MenteeController.java +++ b/src/main/java/com/wcc/platform/controller/MenteeController.java @@ -1,9 +1,14 @@ package com.wcc.platform.controller; +import com.wcc.platform.configuration.security.RequiresPermission; +import com.wcc.platform.domain.auth.Permission; +import com.wcc.platform.domain.platform.mentorship.ApplicationRejectRequest; import com.wcc.platform.domain.platform.mentorship.Mentee; import com.wcc.platform.domain.platform.mentorship.MenteeRegistration; +import com.wcc.platform.service.MenteeAdminService; import com.wcc.platform.service.MenteeService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -13,6 +18,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -31,6 +38,7 @@ public class MenteeController { private final MenteeService menteeService; + private final MenteeAdminService menteeAdminService; /** * API to create mentee. @@ -48,14 +56,66 @@ public ResponseEntity createMentee( } /** - * Retrieves a list of all registered mentees. + * Retrieves a list of all active mentees (status_id = 1). * - * @return a list of Mentee existent mentees + * @return a list of active mentees */ @GetMapping("/mentees") - @Operation(summary = "API to list all mentees") + @Operation(summary = "API to list all active mentees") @ResponseStatus(HttpStatus.OK) public ResponseEntity> listMentees() { return new ResponseEntity<>(menteeService.getAllMentees(), HttpStatus.OK); } + + /** + * Retrieves all mentees with PENDING status awaiting admin review. + * + * @return a list of pending mentees + */ + @GetMapping("/mentees/pending") + @RequiresPermission(Permission.MENTEE_APPROVE) + @Operation( + summary = "Get all pending mentees awaiting admin activation", + security = {@SecurityRequirement(name = "apiKey"), @SecurityRequirement(name = "bearerAuth")}) + @ResponseStatus(HttpStatus.OK) + public ResponseEntity> getPendingMentees() { + return ResponseEntity.ok(menteeAdminService.getPendingMentees()); + } + + /** + * Admin activates a mentee by setting their profile status to ACTIVE. + * + * @param menteeId The mentee ID + * @return the activated mentee + */ + @PatchMapping("/mentees/{menteeId}/activate") + @RequiresPermission(Permission.MENTEE_APPROVE) + @Operation( + summary = "Admin activates a mentee (sets status to ACTIVE)", + security = {@SecurityRequirement(name = "apiKey"), @SecurityRequirement(name = "bearerAuth")}) + @ResponseStatus(HttpStatus.OK) + public ResponseEntity activateMentee( + @Parameter(description = "Mentee ID") @PathVariable final Long menteeId) { + return ResponseEntity.ok(menteeAdminService.activateMentee(menteeId)); + } + + /** + * Admin rejects a mentee by setting their profile status to REJECTED and rejecting all pending + * applications. + * + * @param menteeId The mentee ID + * @param request Rejection request containing the reason + * @return the rejected mentee + */ + @PatchMapping("/mentees/{menteeId}/reject") + @RequiresPermission(Permission.MENTEE_APPROVE) + @Operation( + summary = "Admin rejects a mentee (sets status to REJECTED and rejects all applications)", + security = {@SecurityRequirement(name = "apiKey"), @SecurityRequirement(name = "bearerAuth")}) + @ResponseStatus(HttpStatus.OK) + public ResponseEntity rejectMentee( + @Parameter(description = "Mentee ID") @PathVariable final Long menteeId, + @Valid @RequestBody final ApplicationRejectRequest request) { + return ResponseEntity.ok(menteeAdminService.rejectMentee(menteeId, request.reason())); + } } diff --git a/src/main/java/com/wcc/platform/repository/MenteeRepository.java b/src/main/java/com/wcc/platform/repository/MenteeRepository.java index fce0585a..e369e4fe 100644 --- a/src/main/java/com/wcc/platform/repository/MenteeRepository.java +++ b/src/main/java/com/wcc/platform/repository/MenteeRepository.java @@ -1,5 +1,6 @@ package com.wcc.platform.repository; +import com.wcc.platform.domain.platform.member.ProfileStatus; import com.wcc.platform.domain.platform.mentorship.Mentee; import java.util.List; @@ -15,4 +16,21 @@ public interface MenteeRepository extends CrudRepository { * @return list of mentees */ List getAll(); + + /** + * Return all mentees with the given profile status. + * + * @param status the profile status to filter by + * @return list of mentees with the given status + */ + List findByStatus(ProfileStatus status); + + /** + * Update the profile status of a mentee. + * + * @param menteeId the mentee ID + * @param status the new profile status + * @return the updated mentee + */ + Mentee updateProfileStatus(Long menteeId, ProfileStatus status); } diff --git a/src/main/java/com/wcc/platform/repository/postgres/mentorship/PostgresMenteeRepository.java b/src/main/java/com/wcc/platform/repository/postgres/mentorship/PostgresMenteeRepository.java index 12e9518f..0431a0f8 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/mentorship/PostgresMenteeRepository.java +++ b/src/main/java/com/wcc/platform/repository/postgres/mentorship/PostgresMenteeRepository.java @@ -24,6 +24,10 @@ public class PostgresMenteeRepository implements MenteeRepository { private static final String SQL_GET_BY_ID = "SELECT * FROM mentees WHERE mentee_id = ?"; private static final String SQL_DELETE_BY_ID = "DELETE FROM mentees WHERE mentee_id = ?"; private static final String SELECT_ALL_MENTEES = "SELECT * FROM mentees"; + private static final String SELECT_BY_STATUS = + "SELECT * FROM mentees WHERE mentees_profile_status = ?"; + private static final String SQL_SET_STATUS = + "UPDATE mentees SET mentees_profile_status = ? WHERE mentee_id = ?"; private static final String SQL_INSERT_MENTEE = "INSERT INTO mentees (mentee_id, mentees_profile_status, bio, years_experience, " + "spoken_languages, available_hs_month) VALUES (?, ?, ?, ?, ?, ?)"; @@ -131,6 +135,20 @@ public void deleteById(final Long menteeId) { jdbc.update(SQL_DELETE_BY_ID, menteeId); } + @Override + public List findByStatus(final ProfileStatus status) { + return jdbc.query(SELECT_BY_STATUS, (rs, rowNum) -> menteeMapper.mapRowToMentee(rs), + status.getStatusId()); + } + + @Override + public Mentee updateProfileStatus(final Long menteeId, final ProfileStatus status) { + jdbc.update(SQL_SET_STATUS, status.getStatusId(), menteeId); + return findById(menteeId) + .orElseThrow( + () -> new MenteeNotSavedException("Mentee not found after status update: " + menteeId)); + } + private void updateMenteeDetails(final Mentee mentee, final Long memberId) { final var profileStatus = mentee.getProfileStatus(); final var skills = mentee.getSkills(); diff --git a/src/main/java/com/wcc/platform/service/MenteeAdminService.java b/src/main/java/com/wcc/platform/service/MenteeAdminService.java new file mode 100644 index 00000000..20bac975 --- /dev/null +++ b/src/main/java/com/wcc/platform/service/MenteeAdminService.java @@ -0,0 +1,78 @@ +package com.wcc.platform.service; + +import com.wcc.platform.domain.exceptions.ContentNotFoundException; +import com.wcc.platform.domain.platform.member.ProfileStatus; +import com.wcc.platform.domain.platform.mentorship.ApplicationStatus; +import com.wcc.platform.domain.platform.mentorship.Mentee; +import com.wcc.platform.domain.platform.mentorship.MenteeApplication; +import com.wcc.platform.repository.MenteeApplicationRepository; +import com.wcc.platform.repository.MenteeRepository; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** Service for admin operations on mentees (activate, reject, list pending). */ +@Slf4j +@Service +@AllArgsConstructor +public class MenteeAdminService { + + private final MenteeRepository menteeRepository; + private final MenteeApplicationRepository registrationsRepo; + + /** + * Return all mentees with PENDING profile status awaiting admin review. + * + * @return List of pending mentees. + */ + public List getPendingMentees() { + final var pendingMentees = menteeRepository.findByStatus(ProfileStatus.PENDING); + return pendingMentees == null ? List.of() : pendingMentees; + } + + /** + * Activate a mentee by setting their profile status to ACTIVE. + * + * @param menteeId the mentee ID + * @return the updated mentee + * @throws ContentNotFoundException if mentee not found + */ + @Transactional + public Mentee activateMentee(final Long menteeId) { + menteeRepository + .findById(menteeId) + .orElseThrow(() -> new ContentNotFoundException("Mentee not found: " + menteeId)); + + final Mentee updated = menteeRepository.updateProfileStatus(menteeId, ProfileStatus.ACTIVE); + log.info("Mentee {} activated by admin", menteeId); + return updated; + } + + /** + * Reject a mentee by setting their profile status to REJECTED and rejecting all pending + * applications. + * + * @param menteeId the mentee ID + * @param reason the reason for rejection + * @return the updated mentee + * @throws ContentNotFoundException if mentee not found + */ + @Transactional + public Mentee rejectMentee(final Long menteeId, final String reason) { + menteeRepository + .findById(menteeId) + .orElseThrow(() -> new ContentNotFoundException("Mentee not found: " + menteeId)); + + final List pending = registrationsRepo.findPendingByMenteeId(menteeId); + pending.forEach( + app -> + registrationsRepo.updateStatus( + app.getApplicationId(), ApplicationStatus.REJECTED, reason)); + + final Mentee updated = menteeRepository.updateProfileStatus(menteeId, ProfileStatus.REJECTED); + log.info("Mentee {} rejected by admin, {} applications rejected", menteeId, pending.size()); + return updated; + } +} diff --git a/src/main/java/com/wcc/platform/service/MenteeService.java b/src/main/java/com/wcc/platform/service/MenteeService.java index 06736d96..5a55664e 100644 --- a/src/main/java/com/wcc/platform/service/MenteeService.java +++ b/src/main/java/com/wcc/platform/service/MenteeService.java @@ -3,6 +3,7 @@ import com.wcc.platform.configuration.MentorshipConfig; import com.wcc.platform.domain.exceptions.*; import com.wcc.platform.domain.platform.member.Member; +import com.wcc.platform.domain.platform.member.ProfileStatus; import com.wcc.platform.domain.platform.mentorship.CycleStatus; import com.wcc.platform.domain.platform.mentorship.Mentee; import com.wcc.platform.domain.platform.mentorship.MenteeApplication; @@ -41,16 +42,13 @@ public class MenteeService { private final UserProvisionService userProvisionService; /** - * Return all stored mentees. + * Return all active mentees (status_id = 1). * - * @return List of mentees. + * @return List of active mentees. */ public List getAllMentees() { - final var allMentees = menteeRepository.getAll(); - if (allMentees == null) { - return List.of(); - } - return allMentees; + final var activeMentees = menteeRepository.findByStatus(ProfileStatus.ACTIVE); + return activeMentees == null ? List.of() : activeMentees; } /** diff --git a/src/main/java/com/wcc/platform/service/MenteeWorkflowService.java b/src/main/java/com/wcc/platform/service/MenteeWorkflowService.java index adfc11be..436ea9d7 100644 --- a/src/main/java/com/wcc/platform/service/MenteeWorkflowService.java +++ b/src/main/java/com/wcc/platform/service/MenteeWorkflowService.java @@ -4,14 +4,10 @@ import com.wcc.platform.domain.exceptions.ApplicationNotFoundException; import com.wcc.platform.domain.exceptions.ContentNotFoundException; import com.wcc.platform.domain.exceptions.MentorCapacityExceededException; -import com.wcc.platform.domain.platform.SocialNetwork; -import com.wcc.platform.domain.platform.SocialNetworkType; import com.wcc.platform.domain.platform.mentorship.ApplicationStatus; import com.wcc.platform.domain.platform.mentorship.MenteeApplication; -import com.wcc.platform.domain.platform.mentorship.MenteeApplicationReviewDto; import com.wcc.platform.domain.platform.mentorship.MentorshipCycleEntity; import com.wcc.platform.repository.MenteeApplicationRepository; -import com.wcc.platform.repository.MenteeRepository; import com.wcc.platform.repository.MentorshipCycleRepository; import com.wcc.platform.repository.MentorshipMatchRepository; import java.util.List; @@ -30,7 +26,6 @@ public class MenteeWorkflowService { private final MenteeApplicationRepository applicationRepository; - private final MenteeRepository menteeRepository; private final MentorshipMatchRepository matchRepository; private final MentorshipCycleRepository cycleRepository; @@ -204,122 +199,6 @@ public List getApplicationsByStatus(final ApplicationStatus s return applicationRepository.findByStatus(status); } - /** - * Returns all PENDING priority-1 mentee applications enriched with mentee profile data, for admin - * review. - * - * @return list of review DTOs containing application and mentee details - */ - public List getPendingPriorityOneReviews() { - return applicationRepository - .findByStatusAndPriorityOrder(ApplicationStatus.PENDING, 1) - .stream() - .map(this::toReviewDto) - .toList(); - } - - /** - * Admin approves a mentee by mentee ID. Only the priority-1 PENDING application is moved to - * MENTOR_REVIEWING; all other PENDING applications remain unchanged. - * - * @param menteeId the mentee ID - * @return the approved application - * @throws ContentNotFoundException if no priority-1 PENDING application exists for the mentee - */ - @Transactional - public MenteeApplication approveMenteeByMenteeId(final Long menteeId) { - final List pending = applicationRepository.findPendingByMenteeId(menteeId); - - final MenteeApplication priorityOne = - pending.stream() - .filter(app -> app.getPriorityOrder() == 1) - .findFirst() - .orElseThrow( - () -> - new ContentNotFoundException( - "No pending priority-1 application found for mentee " + menteeId)); - - final MenteeApplication updated = - applicationRepository.updateStatus( - priorityOne.getApplicationId(), ApplicationStatus.MENTOR_REVIEWING, null); - - log.info( - "Mentee {} priority-1 application {} approved and forwarded to mentor {}", - menteeId, - priorityOne.getApplicationId(), - priorityOne.getMentorId()); - - return updated; - } - - /** - * Admin rejects all PENDING applications for a mentee by mentee ID. - * - * @param menteeId the mentee ID - * @param reason the reason for rejection - * @return list of all rejected applications - * @throws ContentNotFoundException if no PENDING applications exist for the mentee - */ - @Transactional - public List rejectMenteeByMenteeId( - final Long menteeId, final String reason) { - final List pending = applicationRepository.findPendingByMenteeId(menteeId); - - if (pending.isEmpty()) { - throw new ContentNotFoundException( - "No pending applications found for mentee " + menteeId); - } - - final List rejected = - pending.stream() - .map( - app -> - applicationRepository.updateStatus( - app.getApplicationId(), ApplicationStatus.REJECTED, reason)) - .toList(); - - log.info( - "All {} pending applications for mentee {} rejected by the Mentorship Team", - rejected.size(), - menteeId); - - return rejected; - } - - private MenteeApplicationReviewDto toReviewDto(final MenteeApplication application) { - return menteeRepository - .findById(application.getMenteeId()) - .map( - mentee -> { - final String linkedinUrl = - mentee.getNetwork() == null - ? null - : mentee.getNetwork().stream() - .filter(n -> n.type() == SocialNetworkType.LINKEDIN) - .findFirst() - .map(SocialNetwork::link) - .orElse(null); - - final Integer yearsExperience = - mentee.getSkills() != null ? mentee.getSkills().yearsExperience() : null; - - return new MenteeApplicationReviewDto( - application.getApplicationId(), - mentee.getId(), - mentee.getFullName(), - mentee.getPosition(), - yearsExperience, - linkedinUrl, - mentee.getSlackDisplayName(), - mentee.getEmail(), - application.getWhyMentor()); - }) - .orElseThrow( - () -> - new ContentNotFoundException( - "Mentee not found for application " + application.getApplicationId())); - } - private MenteeApplication getApplicationOrThrow(final Long applicationId) { return applicationRepository .findById(applicationId) diff --git a/src/test/java/com/wcc/platform/controller/MenteeControllerTest.java b/src/test/java/com/wcc/platform/controller/MenteeControllerTest.java index b399e937..08f90922 100644 --- a/src/test/java/com/wcc/platform/controller/MenteeControllerTest.java +++ b/src/test/java/com/wcc/platform/controller/MenteeControllerTest.java @@ -3,14 +3,19 @@ import static com.wcc.platform.factories.SetupMenteeFactories.createMenteeTest; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.databind.ObjectMapper; import com.wcc.platform.configuration.SecurityConfig; import com.wcc.platform.configuration.TestConfig; -import com.wcc.platform.domain.platform.mentorship.Mentee; +import com.wcc.platform.domain.exceptions.ContentNotFoundException; +import com.wcc.platform.domain.platform.mentorship.ApplicationRejectRequest; +import com.wcc.platform.service.MenteeAdminService; import com.wcc.platform.service.MenteeService; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -32,14 +37,18 @@ class MenteeControllerTest { private static final String API_MENTEES = "/api/platform/v1/mentees"; private static final String API_KEY_HEADER = "X-API-KEY"; private static final String API_KEY_VALUE = "test-api-key"; + private static final String REJECTION_REASON = + "Application does not meet the eligibility criteria for this mentorship cycle"; @Autowired private MockMvc mockMvc; + @Autowired private ObjectMapper objectMapper; @MockBean private MenteeService menteeService; + @MockBean private MenteeAdminService menteeAdminService; @Test @DisplayName("Given valid mentee registration, when creating mentee, then return 201 Created") void shouldCreateMenteeAndReturnCreated() throws Exception { - Mentee mockMentee = createMenteeTest(2L, "Mark", "mark@test.com"); + var mockMentee = createMenteeTest(2L, "Mark", "mark@test.com"); var currentYear = java.time.Year.now(); when(menteeService.saveRegistration(any())).thenReturn(mockMentee); @@ -59,9 +68,10 @@ void shouldCreateMenteeAndReturnCreated() throws Exception { } @Test - @DisplayName("Given mentees exist, when listing mentees, then return 200 OK") + @DisplayName( + "Given active mentees exist, when listing mentees, then return 200 OK with active only") void shouldListMenteesAndReturnOk() throws Exception { - Mentee mockMentee = createMenteeTest(2L, "Mark", "mark@test.com"); + var mockMentee = createMenteeTest(2L, "Mark", "mark@test.com"); when(menteeService.getAllMentees()).thenReturn(List.of(mockMentee)); mockMvc @@ -74,4 +84,115 @@ void shouldListMenteesAndReturnOk() throws Exception { .andExpect(jsonPath("$[0].id", is(2))) .andExpect(jsonPath("$[0].fullName", is("Mark"))); } + + @Test + @DisplayName("Given pending mentees exist, when getting pending mentees, then return 200 OK") + void shouldReturnPendingMenteesAndReturn200() throws Exception { + var mockMentee = createMenteeTest(2L, "Mark", "mark@test.com"); + when(menteeAdminService.getPendingMentees()).thenReturn(List.of(mockMentee)); + + mockMvc + .perform( + MockMvcRequestBuilders.get(API_MENTEES + "/pending") + .header(API_KEY_HEADER, API_KEY_VALUE) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()", is(1))) + .andExpect(jsonPath("$[0].id", is(2))); + } + + @Test + @DisplayName("Given no pending mentees, when getting pending mentees, then return empty list") + void shouldReturnEmptyListWhenNoPendingMentees() throws Exception { + when(menteeAdminService.getPendingMentees()).thenReturn(List.of()); + + mockMvc + .perform( + MockMvcRequestBuilders.get(API_MENTEES + "/pending") + .header(API_KEY_HEADER, API_KEY_VALUE) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()", is(0))); + } + + @Test + @DisplayName( + "Given a pending mentee, when admin activates, then return 200 OK with active mentee") + void shouldActivateMenteeAndReturn200() throws Exception { + var activeMentee = createMenteeTest(10L, "Jane", "jane@test.com"); + when(menteeAdminService.activateMentee(10L)).thenReturn(activeMentee); + + mockMvc + .perform( + MockMvcRequestBuilders.patch(API_MENTEES + "/10/activate") + .header(API_KEY_HEADER, API_KEY_VALUE) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(10))); + } + + @Test + @DisplayName("Given mentee not found, when admin activates, then return 404 NOT_FOUND") + void shouldReturn404WhenActivatingNonExistentMentee() throws Exception { + when(menteeAdminService.activateMentee(99L)) + .thenThrow(new ContentNotFoundException("Mentee not found: 99")); + + mockMvc + .perform( + MockMvcRequestBuilders.patch(API_MENTEES + "/99/activate") + .header(API_KEY_HEADER, API_KEY_VALUE) + .contentType(APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName( + "Given a pending mentee, when admin rejects with reason, then return 200 OK with rejected mentee") + void shouldRejectMenteeAndReturn200() throws Exception { + var rejectedMentee = createMenteeTest(10L, "Jane", "jane@test.com"); + when(menteeAdminService.rejectMentee(anyLong(), anyString())).thenReturn(rejectedMentee); + + final var request = new ApplicationRejectRequest(REJECTION_REASON); + + mockMvc + .perform( + MockMvcRequestBuilders.patch(API_MENTEES + "/10/reject") + .header(API_KEY_HEADER, API_KEY_VALUE) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(10))); + } + + @Test + @DisplayName( + "Given rejection reason is too short, when admin rejects, then return 400 BAD_REQUEST") + void shouldReturn400WhenRejectionReasonTooShort() throws Exception { + final var request = new ApplicationRejectRequest("Short"); + + mockMvc + .perform( + MockMvcRequestBuilders.patch(API_MENTEES + "/10/reject") + .header(API_KEY_HEADER, API_KEY_VALUE) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Given mentee not found, when admin rejects, then return 404 NOT_FOUND") + void shouldReturn404WhenRejectingNonExistentMentee() throws Exception { + when(menteeAdminService.rejectMentee(anyLong(), anyString())) + .thenThrow(new ContentNotFoundException("Mentee not found: 99")); + + final var request = new ApplicationRejectRequest(REJECTION_REASON); + + mockMvc + .perform( + MockMvcRequestBuilders.patch(API_MENTEES + "/99/reject") + .header(API_KEY_HEADER, API_KEY_VALUE) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()); + } } diff --git a/src/test/java/com/wcc/platform/service/MenteeAdminServiceTest.java b/src/test/java/com/wcc/platform/service/MenteeAdminServiceTest.java new file mode 100644 index 00000000..55853b34 --- /dev/null +++ b/src/test/java/com/wcc/platform/service/MenteeAdminServiceTest.java @@ -0,0 +1,122 @@ +package com.wcc.platform.service; + +import static com.wcc.platform.factories.SetupMenteeFactories.createMenteeTest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.wcc.platform.domain.exceptions.ContentNotFoundException; +import com.wcc.platform.domain.platform.member.ProfileStatus; +import com.wcc.platform.domain.platform.mentorship.ApplicationStatus; +import com.wcc.platform.domain.platform.mentorship.MenteeApplication; +import com.wcc.platform.repository.MenteeApplicationRepository; +import com.wcc.platform.repository.MenteeRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +class MenteeAdminServiceTest { + + @Mock private MenteeRepository menteeRepository; + @Mock private MenteeApplicationRepository registrationsRepo; + + private MenteeAdminService service; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + service = new MenteeAdminService(menteeRepository, registrationsRepo); + } + + @Test + @DisplayName( + "Given pending mentees exist, when getting pending mentees, then should return pending mentees") + void shouldReturnPendingMentees() { + final var pendingMentee = createMenteeTest(5L, "Pending Mentee", "pending@wcc.com"); + when(menteeRepository.findByStatus(ProfileStatus.PENDING)).thenReturn(List.of(pendingMentee)); + + final var result = service.getPendingMentees(); + + assertThat(result).hasSize(1); + assertThat(result.getFirst().getId()).isEqualTo(5L); + verify(menteeRepository).findByStatus(ProfileStatus.PENDING); + } + + @Test + @DisplayName( + "Given no pending mentees, when getting pending mentees, then should return empty list") + void shouldReturnEmptyListWhenNoPendingMentees() { + when(menteeRepository.findByStatus(ProfileStatus.PENDING)).thenReturn(List.of()); + + final var result = service.getPendingMentees(); + + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("Given existing mentee, when activating mentee, then should set status to ACTIVE") + void shouldActivateMenteeSuccessfully() { + final var activeMentee = createMenteeTest(10L, "Active Mentee", "active@wcc.com"); + when(menteeRepository.findById(10L)).thenReturn(Optional.of(activeMentee)); + when(menteeRepository.updateProfileStatus(10L, ProfileStatus.ACTIVE)).thenReturn(activeMentee); + + final var result = service.activateMentee(10L); + + assertThat(result).isEqualTo(activeMentee); + verify(menteeRepository).updateProfileStatus(10L, ProfileStatus.ACTIVE); + } + + @Test + @DisplayName( + "Given mentee not found, when activating mentee, then should throw ContentNotFoundException") + void shouldThrowContentNotFoundExceptionWhenActivatingNonExistentMentee() { + when(menteeRepository.findById(99L)).thenReturn(Optional.empty()); + + assertThrows(ContentNotFoundException.class, () -> service.activateMentee(99L)); + } + + @Test + @DisplayName( + "Given existing mentee with pending applications, when rejecting mentee, " + + "then should set status to REJECTED and reject all pending applications") + void shouldRejectMenteeAndAllPendingApplications() { + final var rejectedMentee = createMenteeTest(10L, "Rejected Mentee", "rejected@wcc.com"); + final var pendingApp = + MenteeApplication.builder() + .applicationId(1L) + .menteeId(10L) + .mentorId(20L) + .cycleId(5L) + .priorityOrder(1) + .status(ApplicationStatus.PENDING) + .build(); + final String reason = "Does not meet criteria for this mentorship cycle at this time"; + + when(menteeRepository.findById(10L)).thenReturn(Optional.of(rejectedMentee)); + when(registrationsRepo.findPendingByMenteeId(10L)).thenReturn(List.of(pendingApp)); + when(menteeRepository.updateProfileStatus(10L, ProfileStatus.REJECTED)) + .thenReturn(rejectedMentee); + + final var result = service.rejectMentee(10L, reason); + + assertThat(result).isEqualTo(rejectedMentee); + verify(registrationsRepo).updateStatus(1L, ApplicationStatus.REJECTED, reason); + verify(menteeRepository).updateProfileStatus(10L, ProfileStatus.REJECTED); + } + + @Test + @DisplayName( + "Given mentee not found, when rejecting mentee, then should throw ContentNotFoundException") + void shouldThrowContentNotFoundExceptionWhenRejectingNonExistentMentee() { + when(menteeRepository.findById(99L)).thenReturn(Optional.empty()); + + assertThrows( + ContentNotFoundException.class, + () -> service.rejectMentee(99L, "Some reason for rejection that is long enough")); + } +} diff --git a/src/test/java/com/wcc/platform/service/MenteeServiceTest.java b/src/test/java/com/wcc/platform/service/MenteeServiceTest.java index bc6c150c..a72c20f8 100644 --- a/src/test/java/com/wcc/platform/service/MenteeServiceTest.java +++ b/src/test/java/com/wcc/platform/service/MenteeServiceTest.java @@ -274,15 +274,16 @@ void shouldThrowExceptionWhenPriorityDuplicatedInRequest() { } @Test - @DisplayName("Given has mentees, when getting all mentees, then should return all") - void testGetAllMentees() { - List mentees = List.of(mentee); - when(menteeRepository.getAll()).thenReturn(mentees); + @DisplayName( + "Given active mentees exist, when getting all mentees, then should return only active mentees") + void shouldReturnOnlyActiveMentees() { + var mentees = List.of(mentee); + when(menteeRepository.findByStatus(ProfileStatus.ACTIVE)).thenReturn(mentees); - List result = menteeService.getAllMentees(); + var result = menteeService.getAllMentees(); assertEquals(mentees, result); - verify(menteeRepository).getAll(); + verify(menteeRepository).findByStatus(ProfileStatus.ACTIVE); } @Test diff --git a/src/test/java/com/wcc/platform/service/MenteeWorkflowServiceTest.java b/src/test/java/com/wcc/platform/service/MenteeWorkflowServiceTest.java index 92b343a2..aa289e7d 100644 --- a/src/test/java/com/wcc/platform/service/MenteeWorkflowServiceTest.java +++ b/src/test/java/com/wcc/platform/service/MenteeWorkflowServiceTest.java @@ -4,21 +4,13 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; -import com.wcc.platform.domain.cms.attributes.Country; import com.wcc.platform.domain.exceptions.ApplicationNotFoundException; import com.wcc.platform.domain.exceptions.ContentNotFoundException; -import com.wcc.platform.domain.platform.SocialNetwork; -import com.wcc.platform.domain.platform.SocialNetworkType; import com.wcc.platform.domain.platform.mentorship.ApplicationStatus; -import com.wcc.platform.domain.platform.mentorship.Mentee; import com.wcc.platform.domain.platform.mentorship.MenteeApplication; -import com.wcc.platform.domain.platform.mentorship.MenteeApplicationReviewDto; -import com.wcc.platform.domain.platform.mentorship.Skills; import com.wcc.platform.repository.MenteeApplicationRepository; -import com.wcc.platform.repository.MenteeRepository; import com.wcc.platform.repository.MentorshipCycleRepository; import com.wcc.platform.repository.MentorshipMatchRepository; -import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -32,7 +24,6 @@ class MenteeWorkflowServiceTest { "Application does not meet the eligibility criteria for this mentorship cycle"; @Mock private MenteeApplicationRepository applicationRepository; - @Mock private MenteeRepository menteeRepository; @Mock private MentorshipMatchRepository matchRepository; @Mock private MentorshipCycleRepository cycleRepository; @@ -41,9 +32,7 @@ class MenteeWorkflowServiceTest { @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - service = - new MenteeWorkflowService( - applicationRepository, menteeRepository, matchRepository, cycleRepository); + service = new MenteeWorkflowService(applicationRepository, matchRepository, cycleRepository); } @Test @@ -96,8 +85,7 @@ void shouldThrowApplicationNotFoundExceptionWhenApprovedApplicationDoesNotExist( } @Test - @DisplayName( - "Given a PENDING application, when admin rejects, then status becomes REJECTED") + @DisplayName("Given a PENDING application, when admin rejects, then status becomes REJECTED") void shouldRejectPendingApplicationAndUpdateStatusToRejected() { final MenteeApplication pending = pendingApplication(1L, 10L, 1); final MenteeApplication rejected = rejectedApplication(1L, 10L); @@ -144,136 +132,6 @@ void shouldThrowApplicationNotFoundExceptionWhenApplicationDoesNotExist() { .hasMessageContaining("Application not found with ID: 99"); } - @Test - @DisplayName( - "Given PENDING priority-1 apps exist, when getting reviews, then enriched DTOs are returned") - void shouldReturnEnrichedDtosForPendingPriorityOneApplications() { - final MenteeApplication app = pendingApplication(1L, 10L, 1); - final Mentee mentee = menteeWithLinkedIn(); - - when(applicationRepository.findByStatusAndPriorityOrder(ApplicationStatus.PENDING, 1)) - .thenReturn(List.of(app)); - when(menteeRepository.findById(10L)).thenReturn(Optional.of(mentee)); - - final List result = service.getPendingPriorityOneReviews(); - - assertThat(result).hasSize(1); - final MenteeApplicationReviewDto dto = result.getFirst(); - assertThat(dto.applicationId()).isEqualTo(1L); - assertThat(dto.menteeId()).isEqualTo(10L); - assertThat(dto.fullName()).isEqualTo("Jane Doe"); - assertThat(dto.email()).isEqualTo("jane@wcc.com"); - assertThat(dto.mentorshipGoal()).isEqualTo("Great mentor"); - assertThat(dto.yearsExperience()).isEqualTo(3); - assertThat(dto.linkedinUrl()).isEqualTo("https://linkedin.com/jane"); - } - - @Test - @DisplayName( - "Given no PENDING priority-1 applications, when getting reviews, then empty list is returned") - void shouldReturnEmptyListWhenNoPendingPriorityOneApplications() { - when(applicationRepository.findByStatusAndPriorityOrder(ApplicationStatus.PENDING, 1)) - .thenReturn(List.of()); - - final List result = service.getPendingPriorityOneReviews(); - - assertThat(result).isEmpty(); - } - - @Test - @DisplayName( - "Given mentee not found for pending app, when getting reviews, then ContentNotFoundException") - void shouldThrowContentNotFoundExceptionWhenMenteeNotFoundForPendingApplication() { - final MenteeApplication app = pendingApplication(1L, 10L, 1); - - when(applicationRepository.findByStatusAndPriorityOrder(ApplicationStatus.PENDING, 1)) - .thenReturn(List.of(app)); - when(menteeRepository.findById(10L)).thenReturn(Optional.empty()); - - assertThatThrownBy(service::getPendingPriorityOneReviews) - .isInstanceOf(ContentNotFoundException.class) - .hasMessageContaining("Mentee not found for application 1"); - } - - @Test - @DisplayName( - "Given mentee has a PENDING priority-1 app, when approving by menteeId, then it is approved") - void shouldApprovePriorityOneApplicationWhenApprovingByMenteeId() { - final MenteeApplication priorityOne = pendingApplication(1L, 10L, 1); - final MenteeApplication priorityTwo = pendingApplication(2L, 10L, 2); - final MenteeApplication approved = reviewingApplication(1L, 10L); - - when(applicationRepository.findPendingByMenteeId(10L)) - .thenReturn(List.of(priorityOne, priorityTwo)); - when(applicationRepository.updateStatus(1L, ApplicationStatus.MENTOR_REVIEWING, null)) - .thenReturn(approved); - - final MenteeApplication result = service.approveMenteeByMenteeId(10L); - - assertThat(result.getStatus()).isEqualTo(ApplicationStatus.MENTOR_REVIEWING); - assertThat(result.getApplicationId()).isEqualTo(1L); - } - - @Test - @DisplayName( - "Given mentee has no PENDING applications, when approving by menteeId, " - + "then ContentNotFoundException is thrown") - void shouldThrowContentNotFoundExceptionWhenNoAppsFoundForMenteeApproval() { - when(applicationRepository.findPendingByMenteeId(10L)).thenReturn(List.of()); - - assertThatThrownBy(() -> service.approveMenteeByMenteeId(10L)) - .isInstanceOf(ContentNotFoundException.class) - .hasMessageContaining("No pending priority-1 application found for mentee 10"); - } - - @Test - @DisplayName( - "Given mentee has PENDING apps but none is priority-1, when approving by menteeId, " - + "then ContentNotFoundException is thrown") - void shouldThrowContentNotFoundExceptionWhenNoPriorityOneAppForMentee() { - final MenteeApplication priorityTwo = pendingApplication(2L, 10L, 2); - - when(applicationRepository.findPendingByMenteeId(10L)).thenReturn(List.of(priorityTwo)); - - assertThatThrownBy(() -> service.approveMenteeByMenteeId(10L)) - .isInstanceOf(ContentNotFoundException.class) - .hasMessageContaining("No pending priority-1 application found for mentee 10"); - } - - @Test - @DisplayName( - "Given mentee has multiple PENDING applications, when rejecting by menteeId, " - + "then all are rejected") - void shouldRejectAllPendingApplicationsWhenRejectingByMenteeId() { - final MenteeApplication app1 = pendingApplication(1L, 10L, 1); - final MenteeApplication app2 = pendingApplication(2L, 10L, 2); - final MenteeApplication rejected1 = rejectedApplication(1L, 10L); - final MenteeApplication rejected2 = rejectedApplication(2L, 10L); - - when(applicationRepository.findPendingByMenteeId(10L)).thenReturn(List.of(app1, app2)); - when(applicationRepository.updateStatus(1L, ApplicationStatus.REJECTED, REJECTION_REASON)) - .thenReturn(rejected1); - when(applicationRepository.updateStatus(2L, ApplicationStatus.REJECTED, REJECTION_REASON)) - .thenReturn(rejected2); - - final List result = service.rejectMenteeByMenteeId(10L, REJECTION_REASON); - - assertThat(result).hasSize(2); - assertThat(result).allMatch(app -> app.getStatus() == ApplicationStatus.REJECTED); - } - - @Test - @DisplayName( - "Given mentee has no PENDING applications, when rejecting by menteeId, " - + "then ContentNotFoundException is thrown") - void shouldThrowContentNotFoundExceptionWhenNoAppsFoundForMenteeRejection() { - when(applicationRepository.findPendingByMenteeId(10L)).thenReturn(List.of()); - - assertThatThrownBy(() -> service.rejectMenteeByMenteeId(10L, REJECTION_REASON)) - .isInstanceOf(ContentNotFoundException.class) - .hasMessageContaining("No pending applications found for mentee 10"); - } - private MenteeApplication pendingApplication( final Long applicationId, final Long menteeId, final int priority) { return MenteeApplication.builder() @@ -312,21 +170,4 @@ private MenteeApplication rejectedApplication( .whyMentor("Great mentor") .build(); } - - private Mentee menteeWithLinkedIn() { - return Mentee.menteeBuilder() - .id(10L) - .fullName("Jane Doe") - .email("jane@wcc.com") - .position("Software Engineer") - .slackDisplayName("jane.doe") - .country(new Country("GB", "United Kingdom")) - .bio("A motivated mentee") - .skills(new Skills(3, List.of(), List.of(), List.of())) - .availableHsMonth(5) - .spokenLanguages(List.of("English")) - .network( - List.of(new SocialNetwork(SocialNetworkType.LINKEDIN, "https://linkedin.com/jane"))) - .build(); - } } \ No newline at end of file From 2aeb7366e2dc2c8f94716634c01fce386e562bd1 Mon Sep 17 00:00:00 2001 From: Adriana Zencke Zimmermann Date: Mon, 6 Apr 2026 22:13:31 +0200 Subject: [PATCH 3/5] chore: Apply Google Java Format to ApplicationStatus enum The enum used 4-space indentation with closing braces on their own lines, inconsistent with the rest of the codebase which follows Google Java Format (2-space indent, fields and methods at the same level as the enum body). This reformats the file to match the enforced style so it no longer diffs noisily against auto-formatted code. No functional change. --- .../mentorship/ApplicationStatus.java | 111 +++++++++--------- 1 file changed, 56 insertions(+), 55 deletions(-) diff --git a/src/main/java/com/wcc/platform/domain/platform/mentorship/ApplicationStatus.java b/src/main/java/com/wcc/platform/domain/platform/mentorship/ApplicationStatus.java index 874df6b5..a8dc5e1d 100644 --- a/src/main/java/com/wcc/platform/domain/platform/mentorship/ApplicationStatus.java +++ b/src/main/java/com/wcc/platform/domain/platform/mentorship/ApplicationStatus.java @@ -4,70 +4,71 @@ import lombok.RequiredArgsConstructor; /** - * Enum representing the status of a mentee application to a mentor. - * Corresponds to the application_status enum in the database. - * Tracks the complete workflow from application submission to matching. + * Enum representing the status of a mentee application to a mentor. Corresponds to the + * application_status enum in the database. Tracks the complete workflow from application submission + * to matching. */ @Getter @RequiredArgsConstructor public enum ApplicationStatus { - PENDING("pending", "Mentee submitted application, awaiting mentor response"), - MENTOR_REVIEWING("mentor_reviewing", "Mentor is actively reviewing the application"), - MENTOR_ACCEPTED("mentor_accepted", "Mentor accepted, awaiting team confirmation"), - MENTOR_DECLINED("mentor_declined", "Mentor declined this application"), - MATCHED("matched", "Successfully matched and confirmed"), - DROPPED("dropped", "Mentee withdrew application"), - REJECTED("rejected", "Rejected by Mentorship Team"), - EXPIRED("expired", "Application expired (no response within timeframe)"); + PENDING("pending", "Mentee submitted application, awaiting mentor response"), + MENTOR_REVIEWING("mentor_reviewing", "Mentor is actively reviewing the application"), + MENTOR_ACCEPTED("mentor_accepted", "Mentor accepted, awaiting team confirmation"), + MENTOR_DECLINED("mentor_declined", "Mentor declined this application"), + MATCHED("matched", "Successfully matched and confirmed"), + DROPPED("dropped", "Mentee withdrew application"), + REJECTED("rejected", "Rejected by Mentorship Team"), + EXPIRED("expired", "Application expired (no response within timeframe)"); - private final String value; - private final String description; + private final String value; - /** - * Get ApplicationStatus from database string value. - * - * @param value the database string value - * @return the corresponding ApplicationStatus - * @throws IllegalArgumentException if the value doesn't match any enum - */ - public static ApplicationStatus fromValue(final String value) { - for (final ApplicationStatus status : values()) { - if (status.value.equalsIgnoreCase(value)) { - return status; - } - } - throw new IllegalArgumentException("Unknown application status: " + value); - } + private final String description; - /** - * Check if the application is in a terminal state (no further changes expected). - * - * @return true if status is terminal - */ - public boolean isTerminal() { - return this == MATCHED || this == REJECTED || this == DROPPED || this == EXPIRED; + /** + * Get ApplicationStatus from database string value. + * + * @param value the database string value + * @return the corresponding ApplicationStatus + * @throws IllegalArgumentException if the value doesn't match any enum + */ + public static ApplicationStatus fromValue(final String value) { + for (final ApplicationStatus status : values()) { + if (status.value.equalsIgnoreCase(value)) { + return status; + } } + throw new IllegalArgumentException("Unknown application status: " + value); + } - /** - * Check if the application is pending mentor action. - * - * @return true if awaiting mentor response - */ - public boolean isPendingMentorAction() { - return this == PENDING || this == MENTOR_REVIEWING; - } + /** + * Check if the application is in a terminal state (no further changes expected). + * + * @return true if status is terminal + */ + public boolean isTerminal() { + return this == MATCHED || this == REJECTED || this == DROPPED || this == EXPIRED; + } - /** - * Check if the application has been accepted by mentor. - * - * @return true if mentor accepted - */ - public boolean isMentorAccepted() { - return this == MENTOR_ACCEPTED || this == MATCHED; - } + /** + * Check if the application is pending mentor action. + * + * @return true if awaiting mentor response + */ + public boolean isPendingMentorAction() { + return this == PENDING || this == MENTOR_REVIEWING; + } - @Override - public String toString() { - return value; - } + /** + * Check if the application has been accepted by mentor. + * + * @return true if mentor accepted + */ + public boolean isMentorAccepted() { + return this == MENTOR_ACCEPTED || this == MATCHED; + } + + @Override + public String toString() { + return value; + } } From 348c347cbb7c72a604d3cce8476a4a2b9c80cd7d Mon Sep 17 00:00:00 2001 From: Adriana Zencke Zimmermann Date: Mon, 6 Apr 2026 22:35:08 +0200 Subject: [PATCH 4/5] fix: Remove unused DTO and apply Java 21 list idioms in tests MenteeApplicationReviewDto was left over from an earlier design iteration and had no callers; removing it prevents confusion for future contributors. Also replace List.get(0)/get(1) with getFirst()/getLast() in the integration test to align with the Java 21 style used elsewhere in the codebase. --- .../MenteeApplicationReviewDto.java | 24 ------------------- ...onRepositoryNewMethodsIntegrationTest.java | 4 ++-- 2 files changed, 2 insertions(+), 26 deletions(-) delete mode 100644 src/main/java/com/wcc/platform/domain/platform/mentorship/MenteeApplicationReviewDto.java diff --git a/src/main/java/com/wcc/platform/domain/platform/mentorship/MenteeApplicationReviewDto.java b/src/main/java/com/wcc/platform/domain/platform/mentorship/MenteeApplicationReviewDto.java deleted file mode 100644 index 3e0fe8a5..00000000 --- a/src/main/java/com/wcc/platform/domain/platform/mentorship/MenteeApplicationReviewDto.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.wcc.platform.domain.platform.mentorship; - -/** - * DTO for admin review of a pending mentee application. - * - *

Combines application details with mentee profile information to support the admin review - * workflow for priority-1 PENDING applications. - * - * @param applicationId application ID - * @param menteeId mentee ID - * @param fullName mentee full name - * @param position current job position - * @param yearsExperience years of experience (may be null) - */ -public record MenteeApplicationReviewDto( - Long applicationId, - Long menteeId, - String fullName, - String position, - Integer yearsExperience, - String linkedinUrl, - String slackDisplayName, - String email, - String mentorshipGoal) {} diff --git a/src/testInt/java/com/wcc/platform/repository/postgres/PostgresMenteeApplicationRepositoryNewMethodsIntegrationTest.java b/src/testInt/java/com/wcc/platform/repository/postgres/PostgresMenteeApplicationRepositoryNewMethodsIntegrationTest.java index 4d7709af..dfa15354 100644 --- a/src/testInt/java/com/wcc/platform/repository/postgres/PostgresMenteeApplicationRepositoryNewMethodsIntegrationTest.java +++ b/src/testInt/java/com/wcc/platform/repository/postgres/PostgresMenteeApplicationRepositoryNewMethodsIntegrationTest.java @@ -195,8 +195,8 @@ void shouldReturnAppsOrderedByPriorityWhenFindingPendingByMenteeId() { applicationRepository.findPendingByMenteeId(mentee.getId()); assertThat(result).hasSize(2); - assertThat(result.get(0).getPriorityOrder()).isEqualTo(1); - assertThat(result.get(1).getPriorityOrder()).isEqualTo(2); + assertThat(result.getFirst().getPriorityOrder()).isEqualTo(1); + assertThat(result.getLast().getPriorityOrder()).isEqualTo(2); } private MenteeApplication createApplication( From 5da075afc127164f828248636d282c5923c7a038 Mon Sep 17 00:00:00 2001 From: Adriana Zencke Zimmermann Date: Mon, 6 Apr 2026 22:37:35 +0200 Subject: [PATCH 5/5] chore: remove unused @SuppressWarnings annotation in MenteeApplicationController --- .../com/wcc/platform/controller/MenteeApplicationController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/wcc/platform/controller/MenteeApplicationController.java b/src/main/java/com/wcc/platform/controller/MenteeApplicationController.java index 0c442a43..e8dca3f9 100644 --- a/src/main/java/com/wcc/platform/controller/MenteeApplicationController.java +++ b/src/main/java/com/wcc/platform/controller/MenteeApplicationController.java @@ -42,7 +42,6 @@ + "and the mentee itself.") @AllArgsConstructor @Validated -@SuppressWarnings({"PMD.ExcessiveImports"}) public class MenteeApplicationController { private final MenteeWorkflowService applicationService;