From f395c7167f24e51c20dcb72ed77d944268893926 Mon Sep 17 00:00:00 2001 From: LarryMg Date: Mon, 8 Sep 2025 11:05:24 +0300 Subject: [PATCH] Implementing features for promoting graduate students to alumni status. --- doc/api.yml | 97 ++++++++++++ .../rest/controller/StudentController.java | 22 +++ .../rest/mapper/StatusEnumMapper.java | 1 + .../java/school/hei/haapi/model/User.java | 3 +- .../service/StudentElevationService.java | 91 +++++++++++ .../service/StudentElevationServiceTest.java | 144 ++++++++++++++++++ 6 files changed, 357 insertions(+), 1 deletion(-) mode change 100644 => 100755 src/main/java/school/hei/haapi/endpoint/rest/controller/StudentController.java mode change 100644 => 100755 src/main/java/school/hei/haapi/endpoint/rest/mapper/StatusEnumMapper.java mode change 100644 => 100755 src/main/java/school/hei/haapi/model/User.java create mode 100755 src/main/java/school/hei/haapi/service/StudentElevationService.java create mode 100755 src/test/java/school/hei/haapi/service/StudentElevationServiceTest.java diff --git a/doc/api.yml b/doc/api.yml index 89b2b91575..e03fef1e20 100644 --- a/doc/api.yml +++ b/doc/api.yml @@ -722,6 +722,102 @@ paths: $ref: '#/components/responses/429' '500': $ref: '#/components/responses/500' + /students/{studentId}/elevate: + put: + tags: + - Students + summary: Elevate a single student to ALUMNI + operationId: elevateStudentToAlumni + parameters: + - name: studentId + in: path + required: true + description: id of the student + schema: + type: string + responses: + '200': + description: Elevation result + content: + application/json: + schema: + type: object + properties: + id: + type: string + example: "STD25000" + elevated: + type: boolean + example: true + error: + type: string + nullable: true + example: null + '400': + description: Business logic error + content: + application/json: + schema: + type: object + properties: + id: + type: string + example: "STD25000" + elevated: + type: boolean + example: false + error: + type: string + example: "Unsufficient credits" + /students/elevate: + put: + tags: + - Students + summary: Elevate a group of students to ALUMNI + operationId: elevateStudentGroupToAlumni + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: string + example: ["G1"] + responses: + '200': + description: Elevations results + content: + application/json: + schema: + type: array + properties: + id: + type: string + example: "STD25000" + elevated: + type: boolean + example: true + error: + type: string + nullable: true + example: null + '400': + description: Business logic error + content: + application/json: + schema: + type: array + properties: + id: + type: string + example: "STD25000" + elevated: + type: boolean + example: false + error: + type: string + example: "Unsufficient credits" /monitors/{id}: put: tags: @@ -6436,6 +6532,7 @@ components: - ENABLED - DISABLED - SUSPENDED + - ALUMNI FileType: type: string enum: diff --git a/src/main/java/school/hei/haapi/endpoint/rest/controller/StudentController.java b/src/main/java/school/hei/haapi/endpoint/rest/controller/StudentController.java old mode 100644 new mode 100755 index 385388eea7..5a71b5163c --- a/src/main/java/school/hei/haapi/endpoint/rest/controller/StudentController.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/controller/StudentController.java @@ -4,10 +4,14 @@ import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; import static school.hei.haapi.model.User.Role.STUDENT; +import java.lang.Exception; import java.time.Instant; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -27,6 +31,7 @@ import school.hei.haapi.model.PageFromOne; import school.hei.haapi.model.User; import school.hei.haapi.service.GroupFlowService; +import school.hei.haapi.service.StudentElevationService; import school.hei.haapi.service.UserService; @RestController @@ -37,6 +42,7 @@ public class StudentController { private final GroupFlowService groupFlowService; private final GroupFlowMapper groupFlowMapper; private final StatusEnumMapper statusEnumMapper; + private final StudentElevationService studentElevationService; private final SexEnumMapper sexEnumMapper; private final CoordinatesValidator validator; @@ -121,6 +127,22 @@ public Student updateStudent( userService.updateUser(userMapper.toDomain(toUpdate), studentId)); } + @PutMapping("/students/{studentId}/elevate") + public ResponseEntity> elevateStudent(@PathVariable String studentId) { + List> results = studentElevationService.elevateStudentByUserIdOrGroupId(List.of(studentId)); + return ResponseEntity.ok(results.getFirst()); + } + + @PutMapping("/students/elevate") + public ResponseEntity>> elevateStudents(@RequestBody List groupId) { + try { + List> results = studentElevationService.elevateStudentByUserIdOrGroupId(groupId); + return ResponseEntity.ok(results); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(List.of(Map.of("error", "Unexpected error: " + e.getMessage()))); + } + } + @PostMapping("/students/{id}/group_flows") public List moveOrDeleteStudentInGroup( @PathVariable(name = "id") String id, @RequestBody List createGroupFlow) { diff --git a/src/main/java/school/hei/haapi/endpoint/rest/mapper/StatusEnumMapper.java b/src/main/java/school/hei/haapi/endpoint/rest/mapper/StatusEnumMapper.java old mode 100644 new mode 100755 index 5310c972a1..c4dd9d0e18 --- a/src/main/java/school/hei/haapi/endpoint/rest/mapper/StatusEnumMapper.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/mapper/StatusEnumMapper.java @@ -15,6 +15,7 @@ public EnableStatus toRestStatus(User.Status status) { case ENABLED -> EnableStatus.ENABLED; case DISABLED -> EnableStatus.DISABLED; case SUSPENDED -> EnableStatus.SUSPENDED; + case ALUMNI -> EnableStatus.ALUMNI; default -> throw new BadRequestException("Unexpected type " + status); }; } diff --git a/src/main/java/school/hei/haapi/model/User.java b/src/main/java/school/hei/haapi/model/User.java old mode 100644 new mode 100755 index 3be38f5b1a..cdf0d44350 --- a/src/main/java/school/hei/haapi/model/User.java +++ b/src/main/java/school/hei/haapi/model/User.java @@ -254,7 +254,8 @@ public enum Sex { public enum Status { ENABLED, DISABLED, - SUSPENDED; + SUSPENDED, + ALUMNI; } public enum Role { diff --git a/src/main/java/school/hei/haapi/service/StudentElevationService.java b/src/main/java/school/hei/haapi/service/StudentElevationService.java new file mode 100755 index 0000000000..b6cf06539b --- /dev/null +++ b/src/main/java/school/hei/haapi/service/StudentElevationService.java @@ -0,0 +1,91 @@ +package school.hei.haapi.service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import lombok.AllArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import school.hei.haapi.endpoint.rest.model.ResultSummary; +import school.hei.haapi.model.User; +import school.hei.haapi.model.exception.NotFoundException; +import school.hei.haapi.repository.UserRepository; +import static school.hei.haapi.model.User.Role.STUDENT; +import static school.hei.haapi.model.User.Status.ALUMNI; + +@Service +@AllArgsConstructor +public class StudentElevationService { + private final GradeResultService gradeResultService; + private final UserRepository userRepository; + private final UserService userService; + + @Transactional + public List> elevateStudentByUserIdOrGroupId(List ids) { + List> results = new ArrayList<>(); + Set processedUserIds = new HashSet<>(); + for(String id : ids) { + try { + User user = userService.findById(id); + if (user != null && !processedUserIds.contains(user.getId())) { + processedUserIds.add(user.getId()); + results.add(elevateStudentToAlumni(user)); + } + continue; + } catch (NotFoundException ignored) { + + } + List users = userRepository.findAllRemainingStudentsByGroupIds(List.of(id), Pageable.unpaged()); + if (users == null || users.isEmpty()) { + Map notFound = new HashMap<>(); + notFound.put("id", id); + notFound.put("elevated", false); + notFound.put("error", "GroupId not found or group contains no students"); + results.add(notFound); + continue; + } + for (User user : users) { + if(user == null) continue; + if(processedUserIds.contains(user.getId())) continue; + processedUserIds.add(user.getId()); + results.add(elevateStudentToAlumni(user)); + } + } + return results; + } + + private Map elevateStudentToAlumni(User user) { + Map result = new HashMap<>(); + result.put("id", user.getId()); + try{ + if(user.getRole() != STUDENT) { + result.put("elevated", false); + result.put("error", "Not a student"); + return result; + } + ResultSummary summary = gradeResultService.getStudentResultSummary(user.getId()); + BigDecimal obtainedCredits = Optional.ofNullable(summary).map(ResultSummary::getObtainedCredits).orElse(BigDecimal.ZERO); + final short licenseCredit = 180; + if(obtainedCredits.compareTo(BigDecimal.valueOf(licenseCredit)) < 0) { + result.put("elevated", false); + result.put("error", "Insufficient credits"); + return result; + } + user.setStatus(ALUMNI); + userRepository.save(user); + result.put("elevated", true); + result.put("error", null); + return result; + } catch (Exception e) { + result.put("elevated", false); + result.put("error", "Unexpected error: " + e.getMessage()); + return result; + } + } +} diff --git a/src/test/java/school/hei/haapi/service/StudentElevationServiceTest.java b/src/test/java/school/hei/haapi/service/StudentElevationServiceTest.java new file mode 100755 index 0000000000..0b6c96db09 --- /dev/null +++ b/src/test/java/school/hei/haapi/service/StudentElevationServiceTest.java @@ -0,0 +1,144 @@ +package school.hei.haapi.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Pageable; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.*; +import static school.hei.haapi.model.User.Status.ALUMNI; + +import school.hei.haapi.endpoint.rest.model.ResultSummary; +import school.hei.haapi.integration.conf.FacadeITMockedThirdParties; +import school.hei.haapi.model.User; +import school.hei.haapi.model.exception.NotFoundException; +import school.hei.haapi.repository.UserRepository; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +@Testcontainers +@AutoConfigureMockMvc +class StudentElevationServiceTest extends FacadeITMockedThirdParties { + @Autowired private StudentElevationService studentElevationService; + @MockBean private UserRepository userRepository; + @MockBean private UserService userService; + @MockBean private GradeResultService gradeResultService; + + private User user; + private User user1; + private User user2; + + @BeforeEach + void setUp() { + user = new User(); + user.setId("STD25000"); + user.setStatus(User.Status.ENABLED); + user.setRole(User.Role.STUDENT); + user.setRef("REF001"); + user.setEmail("student@mail.com"); + user.setLastName("Doe"); + user.setFirstName("J"); + user1 = new User(); + user1.setId("STD25001"); + user1.setStatus(User.Status.ENABLED); + user1.setRole(User.Role.STUDENT); + user1.setRef("REF002"); + user1.setEmail("student1@mail.com"); + user1.setLastName("Bob"); + user1.setFirstName("J"); + user2 = new User(); + user2.setId("STD25002"); + user2.setStatus(User.Status.ENABLED); + user2.setRole(User.Role.STUDENT); + user2.setRef("REF003"); + user2.setEmail("student2@mail.com"); + user2.setLastName("Alice"); + user2.setFirstName("J"); + when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0)); + } + + @Test + void elevate_single_student_success() { + ResultSummary summary = mock(ResultSummary.class); + when(summary.getObtainedCredits()).thenReturn(BigDecimal.valueOf(180)); + // when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(userService.findById(user.getId())).thenReturn(user); + when(gradeResultService.getStudentResultSummary(user.getId())).thenReturn(summary); + List> results = studentElevationService.elevateStudentByUserIdOrGroupId(List.of(user.getId())); + assertEquals(1, results.size()); + Map result = results.getFirst(); + assertEquals(user.getId(), result.get("id")); + assertEquals(Boolean.TRUE, result.get("elevated")); + assertNull(result.get("error")); + assertEquals(ALUMNI, user.getStatus()); + } + + @Test + void elevate_single_non_student_returns_error() { + user.setRole(User.Role.STAFF_MEMBER); + when(userService.findById(user.getId())).thenReturn(user); + List> results = studentElevationService.elevateStudentByUserIdOrGroupId(List.of(user.getId())); + assertEquals(1, results.size()); + Map result = results.getFirst(); + assertEquals(user.getId(), result.get("id")); + assertEquals(Boolean.FALSE, result.get("elevated")); + assertEquals("Not a student", result.get("error")); + } + + @Test + void elevate_single_student_with_not_enough_credits_returns_error() { + ResultSummary summary = mock(ResultSummary.class); + when(summary.getObtainedCredits()).thenReturn(BigDecimal.valueOf(150)); + when(userService.findById(user.getId())).thenReturn(user); + when(gradeResultService.getStudentResultSummary(user.getId())).thenReturn(summary); + List> results = studentElevationService.elevateStudentByUserIdOrGroupId(List.of(user.getId())); + assertEquals(1, results.size()); + Map result = results.getFirst(); + assertEquals(user.getId(), result.get("id")); + assertEquals(Boolean.FALSE, result.get("elevated")); + assertEquals("Insufficient credits", result.get("error")); + } + + @Test + void elevate_group_notFoundOrEmpty_returns_error(){ + String groupId = "A1"; + when(userService.findById(groupId)).thenThrow(new NotFoundException("not found")); + when(userRepository.findAllRemainingStudentsByGroupIds(List.of(groupId), Pageable.unpaged())).thenReturn(List.of()); + List> results = studentElevationService.elevateStudentByUserIdOrGroupId(List.of(groupId)); + assertEquals(1, results.size()); + Map result = results.getFirst(); + assertEquals(groupId, result.get("id")); + assertEquals(Boolean.FALSE, result.get("elevated")); + assertEquals("GroupId not found or group contains no students", result.get("error")); + } + + @Test + void elevate_group_processes_each_student_and_avoid_duplicates(){ + String groupeId = "B1"; + when(userService.findById(groupeId)).thenThrow(new NotFoundException("not a user")); + when(userRepository.findAllRemainingStudentsByGroupIds(List.of(groupeId), Pageable.unpaged())).thenReturn(List.of(user1,user2)); + ResultSummary summary1 = mock(ResultSummary.class); + when(summary1.getObtainedCredits()).thenReturn(BigDecimal.valueOf(180)); + ResultSummary summary2 = mock(ResultSummary.class); + when(summary2.getObtainedCredits()).thenReturn(BigDecimal.valueOf(170)); + when(gradeResultService.getStudentResultSummary(user1.getId())).thenReturn(summary1); + when(gradeResultService.getStudentResultSummary(user2.getId())).thenReturn(summary2); + List> results = studentElevationService.elevateStudentByUserIdOrGroupId(List.of(groupeId)); + assertEquals(2, results.size()); + Map result1 = results.stream().filter(m -> m.get("id").equals(user1.getId())).findFirst().get(); + Map result2 = results.stream().filter(m -> m.get("id").equals(user2.getId())).findFirst().get(); + assertEquals(Boolean.TRUE, result1.get("elevated")); + assertNull(result1.get("error")); + assertEquals(ALUMNI, user1.getStatus()); + assertEquals(Boolean.FALSE, result2.get("elevated")); + assertEquals("Insufficient credits", result2.get("error")); + } + +} + +