Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
066dcad
fix: get student repeating year
DyferHerioss Jun 24, 2026
a267e8a
fix: test
DyferHerioss Jun 24, 2026
107bb14
chore: format
DyferHerioss Jun 24, 2026
6afc834
chore: rename class
DyferHerioss Jun 24, 2026
b926005
chore: apply request change
DyferHerioss Jun 25, 2026
bbbd774
chore: format
DyferHerioss Jun 25, 2026
6a31a53
chore: get levels between two date instead before of one date
DyferHerioss Jun 25, 2026
95217b5
feat: replace getCourseResultByIdAndLevel by delegating it to groupFl…
tokyramarozaka Jun 25, 2026
7a5b525
chore: add GroupFlowPeriod dto
tokyramarozaka Jul 3, 2026
3a052de
chore: refactor grade
DyferHerioss Jul 7, 2026
7af7239
chore: rename variable
DyferHerioss Jul 7, 2026
69748c5
chore: remove unused method
DyferHerioss Jul 7, 2026
3b61d07
fix: grade result test
DyferHerioss Jul 9, 2026
d2a0355
fix: warning
DyferHerioss Jul 9, 2026
dcc5e3b
chore: resolve conflict
DyferHerioss Jul 9, 2026
2a5d577
chore: change expression
DyferHerioss Jul 9, 2026
183daed
chore: test for student repeating year
DyferHerioss Jul 9, 2026
7ed611f
Merge branch 'preprod' into fix/grades-for-student-repeating-year
DyferHerioss Jul 9, 2026
e67cdb3
chore: format
DyferHerioss Jul 9, 2026
815432b
chore: remove unused method
DyferHerioss Jul 9, 2026
98678f4
chore: format
DyferHerioss Jul 9, 2026
d6183b6
chore: remove unused method
DyferHerioss Jul 10, 2026
8f3ec40
chore: create record class instead of using map
DyferHerioss Jul 10, 2026
4b47c21
chore: format
DyferHerioss Jul 10, 2026
cd92ba5
chore: apply request change
DyferHerioss Jul 11, 2026
a9f5532
chor: format
DyferHerioss Jul 11, 2026
476748c
fix: sonar check
DyferHerioss Jul 11, 2026
5cf9e13
chore: fiw warning
DyferHerioss Jul 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,6 @@ public List<Fee> getFeesByStudentId(
@RequestParam PageFromOne page,
@RequestParam("page_size") BoundedPageSize pageSize,
@RequestParam(required = false) FeeStatusEnum status) {
// Verify pending mpbs before fetching fees so results reflect the current vola state.
// Orchestrated here to avoid circular dependency (MpbsService -> FeeService).
mpbsVerificationService.verifyPendingMpbsForStudent(studentId);
return feeService.getFeesByStudentId(studentId, page, pageSize, status).stream()
.map(feeMapper::toRestFee)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public ExamGradeStats getExamGradeStats(@PathVariable(value = "exam_id") String
public YearlyResult getYearlyResult(
@PathVariable("student_id") String studentId,
@PathVariable("student_level") StudentLevel studentLevel) {
return gradeResultService.getLeveledYearlyResultByStudentId(studentLevel, studentId);
return gradeResultService.getYearlyResultByStudentIdAndByLevel(studentId, studentLevel);
}

@GetMapping("/students/{student_id}/results_summary")
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/school/hei/haapi/model/dto/GroupFlowPeriod.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package school.hei.haapi.model.dto;

import java.time.Instant;
import school.hei.haapi.model.Group;

public record GroupFlowPeriod(Group group, Instant start, Instant end) {
public boolean contains(Instant instant) {
boolean afterStart = !instant.isBefore(start);
boolean beforeEnd = (end == null) || instant.isBefore(end);
return afterStart && beforeEnd;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package school.hei.haapi.model.dto;

import java.util.List;
import school.hei.haapi.model.CourseAssignment;

public record GroupFlowPeriodCourseAssignment(
GroupFlowPeriod groupFlowPeriod, List<CourseAssignment> courseAssignments) {}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import school.hei.haapi.endpoint.rest.model.StudentLevel;
import school.hei.haapi.model.CourseAssignment;
import school.hei.haapi.model.User;

Expand All @@ -16,10 +15,10 @@ public interface CourseAssignmentRepository extends JpaRepository<CourseAssignme
@Query("SELECT ca FROM CourseAssignment ca JOIN ca.groups g WHERE g.id = :groupId")
List<CourseAssignment> findAllByGroupId(@Param("groupId") String groupId, Pageable pageable);

@Query("SELECT ca FROM CourseAssignment ca JOIN ca.groups g WHERE g.id = :groupId")
List<CourseAssignment> findAllByGroupId(@Param("groupId") String groupId);

List<CourseAssignment> findAllByMainTeacher(User teacher, Pageable pageable);

List<CourseAssignment> findAllByCourseId(String courseId, Pageable pageable);

List<CourseAssignment> findAllByCourse_StudentLevelAndGroupsIn(
StudentLevel courseStudentLevel, List<String> groupIds);
}
21 changes: 0 additions & 21 deletions src/main/java/school/hei/haapi/repository/ExamRepository.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package school.hei.haapi.repository;

import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
Expand All @@ -17,23 +15,4 @@ public interface ExamRepository extends JpaRepository<Exam, String> {
@Query("select e from Exam e where e.courseAssignment.id in :course_assignment_ids ")
List<Exam> findExamsByCourseAssignmentIdIn(
Comment thread
DyferHerioss marked this conversation as resolved.
@Param("course_assignment_ids") List<String> courseAssignmentIds);

@Query(
"select e from Exam e join e.courseAssignment.groups g where g.id = :group_id and "
+ "e.courseAssignment.id = :course_assignment_id")
Page<Exam> findExamsByGroupIdAndCourseAssignmentId(
@Param("group_id") String courseId,
@Param("course_assignment_id") String courseAssignmentId,
Pageable pageable);

@Query(
"""
select distinct u.ref
from Exam e
join e.courseAssignment.groups g
join g.groupFlows gf
join gf.student u
where e.id = :exam_id and gf.student.status = 'ENABLED'
""")
List<String> findStudentRefsByExamId(@Param("exam_id") String examId);
}
17 changes: 15 additions & 2 deletions src/main/java/school/hei/haapi/repository/GroupFlowRepository.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
package school.hei.haapi.repository;

import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import school.hei.haapi.endpoint.rest.model.StudentLevel;
import school.hei.haapi.model.GroupFlow;
import school.hei.haapi.model.User;

@Repository
public interface GroupFlowRepository extends JpaRepository<GroupFlow, String> {
List<GroupFlow> findAllByGroupId(String groupId, Sort sortable);
@Query(
"""
SELECT gf FROM GroupFlow gf
JOIN gf.group g
JOIN CourseAssignment ca ON g MEMBER OF ca.groups
JOIN ca.course c
WHERE gf.student = :student
AND c.studentLevel = :level
""")
List<GroupFlow> findByFlowTypeAndStudentAndLevel(
@Param("student") User student, @Param("level") StudentLevel level);
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public List<CourseAssignment> getByGroupId(
return courseAssignmentRepository.findAllByGroupId(groupId, pageable);
}

@Transactional
public List<CourseAssignment> getByGroupId(String groupId) {
return courseAssignmentRepository.findAllByGroupId(groupId);
}

public CourseAssignment getById(String id) {
return courseAssignmentRepository.getById(id);
}
Expand Down
154 changes: 87 additions & 67 deletions src/main/java/school/hei/haapi/service/CourseResultService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static java.math.MathContext.DECIMAL128;
import static java.util.Comparator.comparing;
import static java.util.Objects.nonNull;
import static java.util.regex.Pattern.compile;
import static school.hei.haapi.endpoint.rest.model.ResultOverviewStatus.INVALIDATED;
import static school.hei.haapi.endpoint.rest.model.ResultOverviewStatus.IN_PROGRESS;
import static school.hei.haapi.endpoint.rest.model.ResultOverviewStatus.NOT_STARTED;
Expand All @@ -16,112 +17,131 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
import school.hei.haapi.endpoint.rest.mapper.CourseMapper;
import school.hei.haapi.endpoint.rest.model.CourseResult;
import school.hei.haapi.endpoint.rest.model.CourseResultStatus;
import school.hei.haapi.endpoint.rest.model.ResultOverviewStatus;
import school.hei.haapi.endpoint.rest.model.StudentLevel;
import school.hei.haapi.model.Course;
import school.hei.haapi.model.CourseAssignment;
import school.hei.haapi.model.Exam;
import school.hei.haapi.model.Group;
import school.hei.haapi.model.User;
import school.hei.haapi.model.dto.GroupFlowPeriod;
import school.hei.haapi.model.dto.GroupFlowPeriodCourseAssignment;
import school.hei.haapi.model.exception.CoursesCreditSumZero;
import school.hei.haapi.model.exception.ExamsCoefficientSumZero;
import school.hei.haapi.repository.dao.GradeDao;
import school.hei.haapi.service.utils.CollectionUtils;

@Service
@AllArgsConstructor
@Slf4j
public class CourseResultService {
private final CourseService courseService;
private final GradeDao gradeDao;
private final CourseMapper courseMapper;
private final ExamService examService;
private final UserService userService;
private final CollectionUtils collectionUtils;
private final CourseAssignmentService courseAssignmentService;
private final GroupFlowService groupFlowService;

private static final BigDecimal VALIDATED_YEAR_CREDIT = BigDecimal.valueOf(30);
private static final BigDecimal VALIDATED_YEAR_AVERAGE = TEN;

private List<Group> findStudentGroupByExams(User student, List<Exam> exams) {
return collectionUtils
.filterDistinctByField(
exams.stream()
.map(exam -> student.findGroupAt(exam.getExaminationDate()))
.filter(Optional::isPresent)
.map(Optional::get)
.toList(),
Group::getId)
.stream()
.toList();
}
private static final Pattern GROUP_TRAILING_DIGITS = compile("\\d+$");

@Transactional
public List<CourseResult> getCourseResultsForLevelOfStudent(
StudentLevel level, String studentId) {
public List<CourseResult> getCourseResultsByStudentIdAndLevel(
String studentId, StudentLevel level) {
var student = userService.getById(studentId);
var coursesForSpecificLevel = courseService.getByStudentLevel(level);
log.info("Course for specific level : {}", coursesForSpecificLevel);
var coursesForSpecificStudent =
collectionUtils.filterDistinctByField(
getCourseInStudentGroup(coursesForSpecificLevel, student), Course::getId);
return coursesForSpecificStudent.stream()
.map(course -> computeCourseResult(course, student))
var studentGroupFlowsAtLevel = groupFlowService.getStudentGroupFlowAtLevel(student, level);
var studentLatestGroupFlowsAtLevel = findLatestGroupFlowPeriods(studentGroupFlowsAtLevel);
var studentGroupCourseAssignmentsAtLevel =
getGroupsCourseAssignmentsByGroupFlowsAtLevel(studentLatestGroupFlowsAtLevel, level);
return studentGroupCourseAssignmentsAtLevel.stream()
.flatMap(
groupCourseAssignments ->
computeStudentCourseResults(groupCourseAssignments, student).stream())
.sorted(comparing(courseResult -> courseResult.getStatus().ordinal()))
.toList();
}

@NotNull
private List<Course> getCourseInStudentGroup(List<Course> coursesForSpecificLevel, User student) {
return coursesForSpecificLevel.stream()
.map(Course::getCourseAssignments)
.filter(courseAssignments -> studentGroupIsAssignedToCourse(courseAssignments, student))
.flatMap(courseAssignments -> courseAssignments.stream().map(CourseAssignment::getCourse))
private List<GroupFlowPeriod> findLatestGroupFlowPeriods(List<GroupFlowPeriod> groupFlowPeriods) {
if (groupFlowPeriods.isEmpty()) {
return List.of();
}

var latestPromotion = latestPromotion(groupFlowPeriods);

return groupFlowPeriods.stream()
.filter(
groupFlowPeriod ->
extractGroupPromotion(groupFlowPeriod.group().getName()).equals(latestPromotion))
.toList();
}

private boolean studentGroupIsAssignedToCourse(
List<CourseAssignment> courseAssignments, User student) {
var courseAssignmentGroupIds =
courseAssignments.stream()
.map(CourseAssignment::getGroups)
.flatMap(groups -> groups.stream().map(Group::getId))
.toList();
var courseAssignmentIds = courseAssignments.stream().map(CourseAssignment::getId).toList();
log.info("All student group flows : {}", student.getGroupFlows());
var studentGroupIds =
student.getGroupFlows().stream().map(groupFlow -> groupFlow.getGroup().getId()).toList();
var exams = examService.getExamsByCourseAssignmentIds(courseAssignmentIds);
if (exams.isEmpty()) {
return courseAssignmentGroupIds.stream().anyMatch(studentGroupIds::contains);
}
private String latestPromotion(List<GroupFlowPeriod> groupFlowPeriods) {
Comment thread
DyferHerioss marked this conversation as resolved.
return extractGroupPromotion(
groupFlowPeriods.stream()
.max(comparing(GroupFlowPeriod::start))
.orElseThrow()
.group()
.getName());
}

var studentExamGroupIds =
findStudentGroupByExams(student, exams).stream().map(Group::getId).toList();
return courseAssignmentGroupIds.stream().anyMatch(studentExamGroupIds::contains);
private String extractGroupPromotion(String groupName) {
Comment thread
DyferHerioss marked this conversation as resolved.
return GROUP_TRAILING_DIGITS.matcher(groupName).replaceAll("");
}

private CourseResult computeCourseResult(Course course, User student) {
var courseExams = examService.getExamsByCourseId(course.getId());
var studentGrades = gradeDao.getStudentGradesByCourseId(course.getId(), student.getId());
var examsOfTheCourse = examService.getExamsByCourseId(course.getId());
var studentGroupInExam = findStudentGroupByExams(student, examsOfTheCourse);
var examOfTheStudent =
examsOfTheCourse.stream()
private List<GroupFlowPeriodCourseAssignment> getGroupsCourseAssignmentsByGroupFlowsAtLevel(
List<GroupFlowPeriod> studentLatestGroupFlows, StudentLevel level) {
return studentLatestGroupFlows.stream()
.map(
groupFlowPeriod ->
getGroupCourseAssignmentsByLevelBetweenPeriod(groupFlowPeriod, level))
.toList();
}

private GroupFlowPeriodCourseAssignment getGroupCourseAssignmentsByLevelBetweenPeriod(
GroupFlowPeriod groupFlowPeriod, StudentLevel level) {
var courseAssignments =
courseAssignmentService.getByGroupId(groupFlowPeriod.group().getId()).stream()
.filter(
courseAssignment -> level.equals(courseAssignment.getCourse().getStudentLevel()))
.filter(
e ->
e.getCourseAssignment().getGroups().stream()
.anyMatch(studentGroupInExam::contains))
courseAssignment -> hasExamOrAssignedBeforeLeave(courseAssignment, groupFlowPeriod))
.toList();
var courseResult = new CourseResult().course(courseMapper.toRest(course));
return new GroupFlowPeriodCourseAssignment(groupFlowPeriod, courseAssignments);
}

private boolean hasExamOrAssignedBeforeLeave(
CourseAssignment courseAssignment, GroupFlowPeriod groupFlowPeriod) {
var courseExams = courseAssignment.getExams();
if (courseExams.isEmpty() && groupFlowPeriod.end() == null) {
return true;
}
return courseExams.stream()
.anyMatch(courseExam -> groupFlowPeriod.contains(courseExam.getExaminationDate()));
}

private List<CourseResult> computeStudentCourseResults(
GroupFlowPeriodCourseAssignment groupCourseAssignments, User student) {
var groupFlowPeriod = groupCourseAssignments.groupFlowPeriod();
var courseAssignments = groupCourseAssignments.courseAssignments();
return courseAssignments.stream()
.map(
courseAssignment ->
getStudentCourseResultByCourseAssignmentWithGroupFlowPeriod(
groupFlowPeriod, courseAssignment, student))
.toList();
}

private CourseResult getStudentCourseResultByCourseAssignmentWithGroupFlowPeriod(
GroupFlowPeriod groupFlowPeriod, CourseAssignment courseAssignment, User student) {
var courseExams = courseAssignment.getExams();
var studentGrades =
gradeDao.getStudentGradesByCourseId(courseAssignment.getCourse().getId(), student.getId());
var courseResult = new CourseResult().course(courseMapper.toRest(courseAssignment.getCourse()));

if (courseExams.isEmpty()) {
if (groupFlowPeriod.end() != null && courseExams.isEmpty()) {
return courseResult.status(CourseResultStatus.NOT_STARTED);
}

Expand All @@ -134,7 +154,7 @@ private CourseResult computeCourseResult(Course course, User student) {

if (studentGrades.isEmpty()) {
return courseResult.status(CourseResultStatus.IN_PROGRESS);
} else if (studentGrades.size() < examOfTheStudent.size()) {
} else if (studentGrades.size() < courseExams.size()) {
return courseResult.status(CourseResultStatus.IN_PROGRESS);
} else if (TEN.compareTo(courseResult.getWeightedAverage()) > 0) {
return courseResult.status(CourseResultStatus.INCOMPLETE);
Expand Down
9 changes: 0 additions & 9 deletions src/main/java/school/hei/haapi/service/ExamService.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,6 @@ public class ExamService {
private final ExamDao examDao;
private final ExamValidator validator;

public List<Exam> getExamsFromAwardedCourseIdAndGroupId(
String groupId, String awardedCourseId, PageFromOne page, BoundedPageSize pageSize) {
Pageable pageable =
PageRequest.of(page.getValue() - 1, pageSize.getValue(), Sort.by(DESC, "examinationDate"));
return examRepository
.findExamsByGroupIdAndCourseAssignmentId(groupId, awardedCourseId, pageable)
.toList();
}

public List<Exam> updateOrSaveAll(List<Exam> exams) {
validator.accept(exams);
return examRepository.saveAll(exams);
Expand Down
12 changes: 7 additions & 5 deletions src/main/java/school/hei/haapi/service/GradeResultService.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ public class GradeResultService {
private static final Duration TRANSCRIPT_GENERATION_TIMEOUT = Duration.ofMinutes(5);
private static final Duration TRANSCRIPT_VALIDATION_DURATION = Duration.ofHours(12);

public YearlyResult getLeveledYearlyResultByStudentId(StudentLevel level, String studentId) {
var courseResults = courseResultService.getCourseResultsForLevelOfStudent(level, studentId);
public YearlyResult getYearlyResultByStudentIdAndByLevel(String studentId, StudentLevel level) {
var courseResults = courseResultService.getCourseResultsByStudentIdAndLevel(studentId, level);
var yearlyResult = new YearlyResult().level(level);

if (courseResults.isEmpty()) return yearlyResult.status(NOT_STARTED);
if (courseResults.isEmpty()) {
return yearlyResult.status(NOT_STARTED);
}

return yearlyResult
.weightedAverage(courseResultService.weightedSumOfCourseResults(courseResults).orElse(null))
Expand All @@ -74,7 +76,7 @@ public YearlyResult getLeveledYearlyResultByStudentId(StudentLevel level, String
public Optional<YearlyResult> findLeveledYearlyResultByStudentId(
StudentLevel level, String studentId) {
try {
return Optional.of(getLeveledYearlyResultByStudentId(level, studentId));
return Optional.of(getYearlyResultByStudentIdAndByLevel(studentId, level));
} catch (CoursesCreditSumZero e) {
log.error(
"Course results for the level {} of the student id {} coefficient sum is 0",
Expand Down Expand Up @@ -157,7 +159,7 @@ private static ResultOverviewStatus resultSummaryStatusFromYearlyResults(

public YearlyResultGenerationTranscript getYearlyResultTranscript(
String studentId, StudentLevel level) {
var studentYearlyResult = getLeveledYearlyResultByStudentId(level, studentId);
var studentYearlyResult = getYearlyResultByStudentIdAndByLevel(studentId, level);

if (NOT_STARTED.equals(studentYearlyResult.getStatus()))
throw new BadRequestException(
Expand Down
Loading
Loading