TraceServiceImpl.java
package fr.avenirsesr.portfolio.trace.domain.service;
import fr.avenirsesr.portfolio.additionalskill.domain.model.AdditionalSkillCategory;
import fr.avenirsesr.portfolio.additionalskill.domain.model.AdditionalSkillProgress;
import fr.avenirsesr.portfolio.additionalskill.domain.port.output.repository.AdditionalSkillProgressRepository;
import fr.avenirsesr.portfolio.ams.domain.model.AMS;
import fr.avenirsesr.portfolio.ams.domain.port.output.repository.AMSRepository;
import fr.avenirsesr.portfolio.common.configuration.domain.model.TraceConfiguration;
import fr.avenirsesr.portfolio.common.data.domain.model.DateFilter;
import fr.avenirsesr.portfolio.common.data.domain.model.PageCriteria;
import fr.avenirsesr.portfolio.common.data.domain.model.PagedResult;
import fr.avenirsesr.portfolio.common.data.domain.model.User;
import fr.avenirsesr.portfolio.common.language.domain.model.enums.ELanguage;
import fr.avenirsesr.portfolio.common.security.domain.exception.UserNotAuthorizedException;
import fr.avenirsesr.portfolio.file.domain.exception.FileNotFoundException;
import fr.avenirsesr.portfolio.file.domain.model.TraceAttachment;
import fr.avenirsesr.portfolio.file.domain.model.shared.File;
import fr.avenirsesr.portfolio.file.domain.port.output.repository.TraceAttachmentRepository;
import fr.avenirsesr.portfolio.program.domain.model.Skill;
import fr.avenirsesr.portfolio.program.domain.model.SkillLevel;
import fr.avenirsesr.portfolio.shared.domain.model.enums.EPortfolioType;
import fr.avenirsesr.portfolio.student.progress.domain.model.SkillLevelProgress;
import fr.avenirsesr.portfolio.student.progress.domain.model.StudentProgress;
import fr.avenirsesr.portfolio.student.progress.domain.port.output.repository.SkillLevelProgressRepository;
import fr.avenirsesr.portfolio.student.progress.domain.port.output.repository.StudentProgressRepository;
import fr.avenirsesr.portfolio.trace.domain.data.*;
import fr.avenirsesr.portfolio.trace.domain.exception.TraceNotFoundException;
import fr.avenirsesr.portfolio.trace.domain.filter.TraceFilter;
import fr.avenirsesr.portfolio.trace.domain.model.*;
import fr.avenirsesr.portfolio.trace.domain.port.input.TraceService;
import fr.avenirsesr.portfolio.trace.domain.port.output.repository.TraceRepository;
import fr.avenirsesr.portfolio.trace.infrastructure.adapter.client.TraceConfigurationClient;
import fr.avenirsesr.portfolio.user.domain.model.Student;
import fr.avenirsesr.portfolio.user.domain.port.input.StudentService;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@AllArgsConstructor
public class TraceServiceImpl implements TraceService {
private static final int MAX_TRACES_OVERVIEW = 3;
private final TraceRepository traceRepository;
private final StudentProgressRepository studentProgressRepository;
private final AdditionalSkillProgressRepository additionalSkillProgressRepository;
private final AMSRepository amsRepository;
private final SkillLevelProgressRepository skillLevelProgressRepository;
private final TraceAttachmentRepository traceAttachmentRepository;
private final StudentService studentService;
private final TraceConfigurationClient traceConfigurationClient;
@Override
public String programNameOfTrace(Trace trace) {
List<StudentProgress> studentProgresses =
studentProgressRepository.findStudentProgressesBySkillLevelProgresses(
trace.getSkillLevels());
return studentProgresses.stream()
.filter(sp -> sp.getTrainingPath().getProgram().isAPC())
.map(sp -> sp.getTrainingPath().getProgram().getName())
.findAny()
.orElse(EPortfolioType.LIFE_PROJECT.name());
}
@Override
public List<Trace> lastTracesOf(User user) {
return traceRepository.findLastsOf(user, MAX_TRACES_OVERVIEW);
}
@Override
public PagedResult<Trace> getTracesView(
User user,
String keyword,
TraceFilter filter,
DateFilter dateFilter,
PageCriteria pageCriteria) {
PagedResult<Trace> pagedResult =
traceRepository.findAll(user, keyword, filter, dateFilter, pageCriteria);
return new PagedResult<>(pagedResult.content(), pagedResult.pageInfo());
}
@Override
public void deleteById(User user, UUID id) {
Trace trace = traceRepository.findById(id).orElseThrow(TraceNotFoundException::new);
checkIfUserIsAuthorizedOnTrace(user, trace);
trace.setAmses(new ArrayList<>());
trace.setSkillLevels(new ArrayList<>());
trace.setAdditionalSkillProgresses(new ArrayList<>());
trace.setDeletedAt(Instant.now());
traceRepository.save(trace);
log.info("Deleted trace {}", trace);
}
@Override
public TracesSummaryData getTracesSummary(User user) {
List<Trace> associatedTraces = traceRepository.findAll(user, true);
List<Trace> unassociatedTraces = traceRepository.findAll(user, false);
TraceConfiguration traceConfiguration = traceConfigurationClient.getTraceConfiguration();
int criticalCount =
unassociatedTraces.stream()
.filter(
t ->
Duration.between(t.getCreatedAt(), Instant.now())
.minus(Duration.ofDays(traceConfiguration.maxRemainingDays()))
.plus(Duration.ofDays(traceConfiguration.maxRemainingDaysBeforeCritical()))
.isPositive())
.toList()
.size();
int warningCount =
unassociatedTraces.stream()
.filter(
t ->
Duration.between(t.getCreatedAt(), Instant.now())
.minus(Duration.ofDays(traceConfiguration.maxRemainingDays()))
.plus(Duration.ofDays(traceConfiguration.maxRemainingDaysBeforeWarning()))
.isPositive())
.toList()
.size();
return new TracesSummaryData(
associatedTraces.size(), unassociatedTraces.size(), warningCount, criticalCount);
}
@Override
public TraceDetailData getTraceDetail(User user, UUID id) {
Trace trace = traceRepository.findById(id).orElseThrow(TraceNotFoundException::new);
checkIfUserIsAuthorizedOnTrace(user, trace);
TraceAttachment traceAttachment = getTraceAttachment(trace);
TraceAssociationsData traceAssociations = getTraceAssociations(user, id);
return new TraceDetailData(
trace.getId(),
trace.getTitle(),
!trace.isUnassociated(),
programNameOfTrace(trace),
trace.isGroup(),
trace.getAiUseJustification().orElse(null),
trace.getPersonalNote().orElse(null),
traceAttachment,
traceAssociations,
trace.getCreatedAt(),
trace.getUpdatedAt());
}
protected TraceAssociationsData getTraceAssociations(User user, UUID id) {
Trace trace = traceRepository.findById(id).orElseThrow(TraceNotFoundException::new);
checkIfUserIsAuthorizedOnTrace(user, trace);
List<SkillLevelAssociationData> skillLevelAssociations = new ArrayList<>();
List<AdditionalSkillAssociationData> additionalSkillAssociations = new ArrayList<>();
for (SkillLevelProgress skillLevelProgress : trace.getSkillLevels()) {
var skillLevel = skillLevelProgress.getSkillLevel();
var skill = skillLevel.getSkill();
if (skillLevelProgress.getAmses() == null || skillLevelProgress.getAmses().isEmpty()) {
skillLevelAssociations.add(
toSkillLevelAssociation(skillLevelProgress, skillLevel, skill, null));
} else {
for (AMS ams : skillLevelProgress.getAmses()) {
skillLevelAssociations.add(
toSkillLevelAssociation(skillLevelProgress, skillLevel, skill, ams));
}
}
}
for (AdditionalSkillProgress additionalSkillProgress : trace.getAdditionalSkillProgresses()) {
additionalSkillAssociations.add(toAdditionalSkillAssociation(additionalSkillProgress));
}
return new TraceAssociationsData(skillLevelAssociations, additionalSkillAssociations);
}
@Override
public Trace createTrace(
User user,
String title,
ELanguage language,
boolean isGroup,
String personalNote,
String aiJustification) {
var trace =
Trace.create(
UUID.randomUUID(), user, title, language, isGroup, aiJustification, personalNote);
return traceRepository.save(trace);
}
@Override
public TraceDetailData updateTrace(
User user,
UUID traceId,
String title,
ELanguage language,
boolean isGroup,
String personalNote,
String aiJustification) {
var trace = traceRepository.findById(traceId).orElseThrow(TraceNotFoundException::new);
checkIfUserIsAuthorizedOnTrace(user, trace);
trace.setTitle(title);
trace.setLanguage(language);
trace.setGroup(isGroup);
trace.setPersonalNote(personalNote);
trace.setAiUseJustification(aiJustification);
var savedTrace = traceRepository.save(trace);
TraceAttachment traceAttachment = getTraceAttachment(savedTrace);
TraceAssociationsData traceAssociations = getTraceAssociations(user, traceId);
return new TraceDetailData(
savedTrace.getId(),
savedTrace.getTitle(),
!savedTrace.isUnassociated(),
programNameOfTrace(savedTrace),
savedTrace.isGroup(),
savedTrace.getAiUseJustification().orElse(null),
savedTrace.getPersonalNote().orElse(null),
traceAttachment,
traceAssociations,
savedTrace.getCreatedAt(),
savedTrace.getUpdatedAt());
}
private TraceAttachment getTraceAttachment(Trace trace) {
return traceAttachmentRepository.findByTrace(trace).stream()
.filter(File::isActiveVersion)
.findFirst()
.orElseThrow(FileNotFoundException::new);
}
@Override
public Optional<LocalDate> getWillBeDeletedAt(Trace trace) {
var config = traceConfigurationClient.getTraceConfiguration();
return trace.isUnassociated()
? Optional.of(
trace
.getCreatedAt()
.plus(Duration.ofDays(config.maxRemainingDays()))
.atZone(ZoneId.systemDefault())
.toLocalDate())
: Optional.empty();
}
@Override
public void associateTrace(
User user,
UUID traceId,
List<UUID> amsIds,
List<UUID> skillLevelIds,
List<UUID> additionalSkillProgressIds) {
var trace = traceRepository.findById(traceId).orElseThrow(TraceNotFoundException::new);
checkIfUserIsAuthorizedOnTrace(user, trace);
var student = studentService.getStudentById(user.getId());
associateAMS(student, trace, amsIds);
associateSkillLevels(student, trace, skillLevelIds);
associateAdditionalSkillProgress(student, trace, additionalSkillProgressIds);
traceRepository.save(trace);
log.info(
"Trace {} successfully associated with amses : {} - skill level progress {} - additonal"
+ " skill progress {}",
trace,
amsIds,
skillLevelIds,
additionalSkillProgressIds);
}
private void associateAMS(Student student, Trace trace, List<UUID> amsIds) {
if (amsIds.stream()
.anyMatch(id -> trace.getAmses().stream().map(AMS::getId).toList().contains(id))) {
log.error(
"{} tried to associate trace with an ams that is already associated. IDS : {}",
student,
amsIds);
throw new UserNotAuthorizedException();
}
var studentAmses = amsRepository.findAllByStudent(student);
amsIds.forEach(
amsId -> {
var ams =
studentAmses.stream()
.filter(a -> amsId.equals(a.getId()))
.findAny()
.orElseThrow(UserNotAuthorizedException::new);
trace.add(ams);
});
}
private void associateSkillLevels(Student student, Trace trace, List<UUID> skillLevelIds) {
if (skillLevelIds.stream()
.anyMatch(
id ->
trace.getSkillLevels().stream()
.map(SkillLevelProgress::getId)
.toList()
.contains(id))) {
log.error(
"{} tried to associate trace with a skill levels that is already associated. IDS : {}",
student,
skillLevelIds);
throw new UserNotAuthorizedException();
}
var studentSkillLevelProgresses = skillLevelProgressRepository.findAllByStudent(student);
skillLevelIds.forEach(
skillLevelId -> {
var skillLevelProgress =
studentSkillLevelProgresses.stream()
.filter(s -> skillLevelId.equals(s.getId()))
.findAny()
.orElseThrow(UserNotAuthorizedException::new);
trace.add(skillLevelProgress);
});
}
private void associateAdditionalSkillProgress(
Student student, Trace trace, List<UUID> additionalSkillProgressIds) {
if (additionalSkillProgressIds.stream()
.anyMatch(
id ->
trace.getAdditionalSkillProgresses().stream()
.map(AdditionalSkillProgress::getId)
.toList()
.contains(id))) {
log.error(
"{} tried to associate trace with an additional skill that is already associated. IDS :"
+ " {}",
student,
additionalSkillProgressIds);
throw new UserNotAuthorizedException();
}
var studentAdditionalSkillProgress =
additionalSkillProgressRepository.findAllByStudent(student);
additionalSkillProgressIds.forEach(
additionalSkillProgressId -> {
var additionalSkillProgress =
studentAdditionalSkillProgress.stream()
.filter(s -> additionalSkillProgressId.equals(s.getId()))
.findAny()
.orElseThrow(UserNotAuthorizedException::new);
trace.add(additionalSkillProgress);
});
}
private void checkIfUserIsAuthorizedOnTrace(User user, Trace trace) {
if (!trace.getUser().getId().equals(user.getId())) {
throw new UserNotAuthorizedException();
}
}
private SkillLevelAssociationData toSkillLevelAssociation(
SkillLevelProgress skillLevelProgress, SkillLevel skillLevel, Skill skill, AMS ams) {
AmsAssociationData amsAssociation =
(ams == null) ? null : new AmsAssociationData(ams.getId(), ams.getTitle(), ams.getStatus());
return new SkillLevelAssociationData(
skillLevelProgress.getId(),
skill.getName(),
skillLevel.getName(),
skillLevelProgress.getStatus(),
amsAssociation);
}
private AdditionalSkillAssociationData toAdditionalSkillAssociation(
AdditionalSkillProgress additionalSkillProgress) {
var skill = additionalSkillProgress.getSkill();
return new AdditionalSkillAssociationData(
additionalSkillProgress.getId(),
skill.getLibelle(),
additionalSkillProgress.getLevel(),
skill.getCategoryPath().stream().map(AdditionalSkillCategory::getLibelle).toList(),
skill.getType());
}
}