Skip to content
Open
21 changes: 21 additions & 0 deletions src/main/java/com/open/spring/mvc/certificate/CertificateType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.open.spring.mvc.certificate;

/**
* Enum representing the type of certificate a user can earn.
* - EXCELLENCE: Awarded when average score >= 88%
* - COMPLETION: Awarded when average score >= 70% but < 88%
*/
public enum CertificateType {
EXCELLENCE("Excellence"),
COMPLETION("Completion");

private final String displayName;

CertificateType(String displayName) {
this.displayName = displayName;
}

public String getDisplayName() {
return displayName;
}
}
48 changes: 48 additions & 0 deletions src/main/java/com/open/spring/mvc/certificate/UserCertificate.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
Expand All @@ -23,6 +25,16 @@ public class UserCertificate {
@ManyToOne(targetEntity = Certificate.class, optional = false)
private Certificate certificate;

@Enumerated(EnumType.STRING)
@Column(name = "certificate_type")
private CertificateType certificateType;

@Column(name = "sprint_name")
private String sprintName;

@Column(name = "average_score")
private Double averageScore;

@Column(nullable = false)
private Date dateIssued;

Expand All @@ -33,6 +45,18 @@ public UserCertificate() {
public UserCertificate(Person person, Certificate certificate) {
this.person = person;
this.certificate = certificate;
this.certificateType = CertificateType.COMPLETION; // default
this.sprintName = "";
this.averageScore = 0.0;
this.dateIssued = new Date();
}

public UserCertificate(Person person, Certificate certificate, CertificateType certificateType, String sprintName, Double averageScore) {
this.person = person;
this.certificate = certificate;
this.certificateType = certificateType;
this.sprintName = sprintName;
this.averageScore = averageScore;
this.dateIssued = new Date();
}

Expand Down Expand Up @@ -67,4 +91,28 @@ public Date getDateIssued() {
public void setDateIssued(Date dateIssued) {
this.dateIssued = dateIssued;
}

public CertificateType getCertificateType() {
return certificateType;
}

public void setCertificateType(CertificateType certificateType) {
this.certificateType = certificateType;
}

public String getSprintName() {
return sprintName;
}

public void setSprintName(String sprintName) {
this.sprintName = sprintName;
}

public Double getAverageScore() {
return averageScore;
}

public void setAverageScore(Double averageScore) {
this.averageScore = averageScore;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package com.open.spring.mvc.certificate;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -16,20 +22,37 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.open.spring.mvc.grades.Grade;
import com.open.spring.mvc.person.Person;
import com.open.spring.mvc.person.PersonJpaRepository;

import jakarta.validation.Valid;

/**
* Request body for direct certificate assignment (admin use)
*/
class UserCertificateRequestBody {
public Long personId;
public Long certificateId;
}

/**
* Request body for certificate request based on assignment completion
*/
class CertificateRequestDTO {
public List<String> formativeAssignments;
public List<String> summativeAssignments;
public String sprintName;
public Long certificateId;
}

@RestController
@RequestMapping("/api/user-certificates")
public class UserCertificateController {

private static final double EXCELLENCE_THRESHOLD = 88.0;
private static final double COMPLETION_THRESHOLD = 70.0;

@Autowired
private UserCertificateRepository userCertificateRepository;

Expand Down Expand Up @@ -81,13 +104,201 @@ public ResponseEntity<?> createUserCertificate(@Valid @RequestBody UserCertifica
return new ResponseEntity<>(newUserCertificate, HttpStatus.CREATED);
}

/**
* Request a certificate based on assignment completion.
*
* The endpoint receives:
* - List of formative assignments
* - List of summative assignments
* - Sprint name
* - Certificate ID
*
* Certificate awarding logic:
* - Average score >= 88%: EXCELLENCE certificate
* - Average score >= 70% but < 88%: COMPLETION certificate
* - Average score < 70%: No certificate awarded
*
* @param requestBody The certificate request containing assignment lists and sprint info
* @param authentication The authenticated user requesting the certificate
* @return ResponseEntity with the awarded certificate or rejection reason
*/
@PostMapping("/request")
public ResponseEntity<?> requestUserCertificate(@Valid @RequestBody UserCertificateRequestBody requestBody) {
// Check if certificate requirements were met
// If met then create UserCertificate entry
// If not met then return appropriate response
public ResponseEntity<?> requestUserCertificate(
@Valid @RequestBody CertificateRequestDTO requestBody,
Authentication authentication) {

// Check authentication
if (authentication == null || !authentication.isAuthenticated()) {
return new ResponseEntity<>("User must be authenticated", HttpStatus.UNAUTHORIZED);
}

// Validate request
if (requestBody.certificateId == null) {
return new ResponseEntity<>("Certificate ID is required", HttpStatus.BAD_REQUEST);
}
if (requestBody.sprintName == null || requestBody.sprintName.trim().isEmpty()) {
return new ResponseEntity<>("Sprint name is required", HttpStatus.BAD_REQUEST);
}

// Get the authenticated user
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String uid = userDetails.getUsername();

return new ResponseEntity<>("Certificate request received", HttpStatus.OK);
Person person = personRepository.findByUid(uid);
if (person == null) {
return new ResponseEntity<>("Person not found", HttpStatus.NOT_FOUND);
}

// Get the certificate
Optional<Certificate> certificateOpt = certificateRepository.findById(requestBody.certificateId);
if (!certificateOpt.isPresent()) {
return new ResponseEntity<>("Certificate not found", HttpStatus.NOT_FOUND);
}
Certificate certificate = certificateOpt.get();

// Combine all assignments
List<String> allAssignments = new ArrayList<>();
if (requestBody.formativeAssignments != null) {
allAssignments.addAll(requestBody.formativeAssignments);
}
if (requestBody.summativeAssignments != null) {
allAssignments.addAll(requestBody.summativeAssignments);
}

if (allAssignments.isEmpty()) {
return new ResponseEntity<>("At least one assignment is required", HttpStatus.BAD_REQUEST);
}

// Fetch grades from the person's JSON field
List<Grade> userGrades = new ArrayList<>();
List<Map<String, Object>> gradesJson = person.getGradesJson();
if (gradesJson != null) {
for (Map<String, Object> m : gradesJson) {
String assignment = m.get("assignment") == null ? null : String.valueOf(m.get("assignment"));
if (allAssignments.contains(assignment)) {
Grade g = new Grade();
g.setUid(uid);
g.setAssignment(assignment);
Object scoreObj = m.get("score");
if (scoreObj != null) {
try {
g.setScore(Double.valueOf(String.valueOf(scoreObj)));
} catch (NumberFormatException e) {
g.setScore(null);
}
}
userGrades.add(g);
}
}
}

// Check if all assignments have been completed
List<String> completedAssignments = new ArrayList<>();
List<String> missingAssignments = new ArrayList<>();

for (String assignment : allAssignments) {
boolean found = userGrades.stream()
.anyMatch(g -> assignment != null && assignment.equals(g.getAssignment()) && g.getScore() != null);
if (found) {
completedAssignments.add(assignment);
} else {
missingAssignments.add(assignment);
}
}

if (!missingAssignments.isEmpty()) {
Map<String, Object> response = new HashMap<>();
response.put("status", "INCOMPLETE");
response.put("message", "Missing grades for some assignments");
response.put("missingAssignments", missingAssignments);
response.put("completedAssignments", completedAssignments);
return new ResponseEntity<>(response, HttpStatus.OK);
}

// Calculate average score
double totalScore = 0.0;
int gradeCount = 0;
for (Grade grade : userGrades) {
if (allAssignments.contains(grade.getAssignment()) && grade.getScore() != null) {
totalScore += grade.getScore();
gradeCount++;
}
}

double averageScore = gradeCount > 0 ? totalScore / gradeCount : 0.0;

// Determine certificate type
CertificateType awardedType = null;
if (averageScore >= EXCELLENCE_THRESHOLD) {
awardedType = CertificateType.EXCELLENCE;
} else if (averageScore >= COMPLETION_THRESHOLD) {
awardedType = CertificateType.COMPLETION;
}

// Check if user doesn't qualify
if (awardedType == null) {
Map<String, Object> response = new HashMap<>();
response.put("status", "NOT_QUALIFIED");
response.put("message", "Average score is below the minimum threshold of " + COMPLETION_THRESHOLD + "%");
response.put("averageScore", Math.round(averageScore * 100.0) / 100.0);
response.put("requiredMinimum", COMPLETION_THRESHOLD);
return new ResponseEntity<>(response, HttpStatus.OK);
}

// Check if user already has this certificate for this sprint
List<UserCertificate> existingCerts = userCertificateRepository.findByPersonId(person.getId());
String requestedSprint = requestBody.sprintName != null ? requestBody.sprintName : "";
for (UserCertificate existingCert : existingCerts) {
if (existingCert.getCertificate() != null && existingCert.getCertificate().getId().equals(certificate.getId())
&& Objects.equals(existingCert.getSprintName() != null ? existingCert.getSprintName() : "", requestedSprint)) {

// User already has certificate for this sprint - check if upgrade is possible
if (existingCert.getCertificateType() == CertificateType.COMPLETION
&& awardedType == CertificateType.EXCELLENCE) {
// Upgrade from COMPLETION to EXCELLENCE
existingCert.setCertificateType(CertificateType.EXCELLENCE);
existingCert.setAverageScore(averageScore);
existingCert.setDateIssued(new Date());
userCertificateRepository.save(existingCert);

Map<String, Object> response = new HashMap<>();
response.put("status", "UPGRADED");
response.put("message", "Certificate upgraded from Completion to Excellence!");
response.put("certificate", existingCert);
response.put("certificateType", awardedType.getDisplayName());
response.put("averageScore", Math.round(averageScore * 100.0) / 100.0);
return new ResponseEntity<>(response, HttpStatus.OK);
}

// Already has same or better certificate
Map<String, Object> response = new HashMap<>();
response.put("status", "ALREADY_EARNED");
response.put("message", "You already have this certificate for " + requestBody.sprintName);
response.put("existingCertificate", existingCert);
response.put("existingType", existingCert.getCertificateType() != null ? existingCert.getCertificateType().getDisplayName() : "Completion");
return new ResponseEntity<>(response, HttpStatus.OK);
}
}

// Award the certificate
UserCertificate newUserCertificate = new UserCertificate(
person,
certificate,
awardedType,
requestBody.sprintName,
averageScore
);
userCertificateRepository.save(newUserCertificate);

Map<String, Object> response = new HashMap<>();
response.put("status", "AWARDED");
response.put("message", "Congratulations! You earned the " + awardedType.getDisplayName() + " certificate!");
response.put("certificate", newUserCertificate);
response.put("certificateType", awardedType.getDisplayName());
response.put("sprintName", requestBody.sprintName);
response.put("averageScore", Math.round(averageScore * 100.0) / 100.0);

return new ResponseEntity<>(response, HttpStatus.CREATED);
}


Expand Down Expand Up @@ -132,4 +343,4 @@ public ResponseEntity<HttpStatus> deleteUserCertificate(@PathVariable Long id) {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/open/spring/mvc/grades/GradeRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.open.spring.mvc.grades;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface GradeRepository extends JpaRepository<Grade, Long> {
List<Grade> findByUid(String uid);

List<Grade> findByAssignment(String assignment);

List<Grade> findByUidAndAssignmentIn(String uid, List<String> assignments);
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,7 @@ <h1>Certificate Manager</h1>
<h2>Add New Certificate</h2>
<form th:action="@{/mvc/certificates/create}" method="post">
<label>Title: <input type="text" name="title" required></label><br>
<label>Quests:
<select name="questIds" class="selectpicker" multiple data-live-search="true">
<option th:each="quest : ${quests}" th:value="${quest.id}" th:text="${quest.name}"></option>
</select>
</label><br>

<button type="submit" class="btn">Submit</button>
<button type="button" class="btn" onclick="closeAddCertificateModal()">Cancel</button>
</form>
Expand Down