diff --git a/backend/src/main/java/ch/puzzle/okr/dto/EvaluationDto.java b/backend/src/main/java/ch/puzzle/okr/dto/EvaluationDto.java index cdc55450e7..277d749bdd 100644 --- a/backend/src/main/java/ch/puzzle/okr/dto/EvaluationDto.java +++ b/backend/src/main/java/ch/puzzle/okr/dto/EvaluationDto.java @@ -1,7 +1,7 @@ package ch.puzzle.okr.dto; -public record EvaluationDto(int objectiveAmount, int completedObjectivesAmount, - int successfullyCompletedObjectivesAmount, int keyResultAmount, int keyResultsOrdinalAmount, - int keyResultsMetricAmount, int keyResultsInTargetOrStretchAmount, int keyResultsInFailAmount, - int keyResultsInCommitAmount, int keyResultsInTargetAmount, int keyResultsInStretchAmount) { +public record EvaluationDto(long objectiveAmount, long completedObjectivesAmount, + long successfullyCompletedObjectivesAmount, long keyResultAmount, long keyResultsOrdinalAmount, + long keyResultsMetricAmount, long keyResultsInTargetOrStretchAmount, long keyResultsInFailAmount, + long keyResultsInCommitAmount, long keyResultsInTargetAmount, long keyResultsInStretchAmount) { } diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/EvaluationViewMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/EvaluationViewMapper.java index ffbbd1699a..3e4a357c43 100644 --- a/backend/src/main/java/ch/puzzle/okr/mapper/EvaluationViewMapper.java +++ b/backend/src/main/java/ch/puzzle/okr/mapper/EvaluationViewMapper.java @@ -2,53 +2,35 @@ import ch.puzzle.okr.dto.EvaluationDto; import ch.puzzle.okr.models.evaluation.EvaluationView; -import ch.puzzle.okr.models.evaluation.EvaluationViewId; +import ch.puzzle.okr.service.business.EvaluationViewBusinessService; +import ch.puzzle.okr.util.TeamQuarterFilter; import java.util.List; import org.springframework.stereotype.Component; @Component public class EvaluationViewMapper { - public EvaluationDto toDto(List evaluationViews) { - int objectiveSum = 0; - int completedObjectivesSum = 0; - int successfullyCompletedObjectivesSum = 0; - int keyResultSum = 0; - int keyResultsOrdinalSum = 0; - int keyResultsMetricSum = 0; - int keyResultsInTargetOrStretchSum = 0; - int keyResultsInFailSum = 0; - int keyResultsInCommitSum = 0; - int keyResultsInTargetSum = 0; - int keyResultsInStretchSum = 0; - for (EvaluationView view : evaluationViews) { - objectiveSum += view.getObjectiveAmount(); - completedObjectivesSum += view.getCompletedObjectivesAmount(); - successfullyCompletedObjectivesSum += view.getSuccessfullyCompletedObjectivesAmount(); - keyResultSum += view.getKeyResultAmount(); - keyResultsOrdinalSum += view.getKeyResultsOrdinalAmount(); - keyResultsMetricSum += view.getKeyResultsMetricAmount(); - keyResultsInTargetOrStretchSum += view.getKeyResultsInTargetOrStretchAmount(); - keyResultsInFailSum += view.getKeyResultsInFailAmount(); - keyResultsInCommitSum += view.getKeyResultsInCommitAmount(); - keyResultsInTargetSum += view.getKeyResultsInTargetAmount(); - keyResultsInStretchSum += view.getKeyResultsInStretchAmount(); - } + private final EvaluationViewBusinessService evaluationService; - return new EvaluationDto(objectiveSum, - completedObjectivesSum, - successfullyCompletedObjectivesSum, - keyResultSum, - keyResultsOrdinalSum, - keyResultsMetricSum, - keyResultsInTargetOrStretchSum, - keyResultsInFailSum, - keyResultsInCommitSum, - keyResultsInTargetSum, - keyResultsInStretchSum); + public EvaluationViewMapper(EvaluationViewBusinessService evaluationService) { + this.evaluationService = evaluationService; } - public List fromDto(List teamIds, Long quarterId) { - return teamIds.stream().map(teamId -> new EvaluationViewId(teamId, quarterId)).toList(); + public EvaluationDto toDto(List views) { + return new EvaluationDto(evaluationService.calculateObjectiveSum(views), + evaluationService.calculateCompletedObjectivesSum(views), + evaluationService.calculateSuccessfullyCompletedObjectivesSum(views), + evaluationService.calculateKeyResultSum(views), + evaluationService.calculateKeyResultsOrdinalSum(views), + evaluationService.calculateKeyResultsMetricSum(views), + evaluationService.calculateKeyResultsInTargetOrStretchSum(views), + evaluationService.calculateKeyResultsInFailSum(views), + evaluationService.calculateKeyResultsInCommitSum(views), + evaluationService.calculateKeyResultsInTargetSum(views), + evaluationService.calculateKeyResultsInStretchSum(views)); } -} + + public TeamQuarterFilter fromDto(List teamIds, Long quarterId) { + return new TeamQuarterFilter(teamIds, quarterId); + } +} \ No newline at end of file diff --git a/backend/src/main/java/ch/puzzle/okr/models/evaluation/EvaluationView.java b/backend/src/main/java/ch/puzzle/okr/models/evaluation/EvaluationView.java index f8c2fc4901..25a2f8bae0 100644 --- a/backend/src/main/java/ch/puzzle/okr/models/evaluation/EvaluationView.java +++ b/backend/src/main/java/ch/puzzle/okr/models/evaluation/EvaluationView.java @@ -1,109 +1,135 @@ package ch.puzzle.okr.models.evaluation; -import jakarta.persistence.EmbeddedId; +import ch.puzzle.okr.models.State; +import ch.puzzle.okr.models.checkin.Zone; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import java.time.Instant; import java.util.Objects; import org.hibernate.annotations.Immutable; @Entity @Immutable public class EvaluationView { - @EmbeddedId - private EvaluationViewId evaluationViewId; - private int objectiveAmount; - private int completedObjectivesAmount; - private int successfullyCompletedObjectivesAmount; + @Id + private Long rowId; - private int keyResultAmount; - private int keyResultsOrdinalAmount; - private int keyResultsMetricAmount; - private int keyResultsInTargetOrStretchAmount; + private Long keyResultId; + private Long objectiveId; + private Long teamId; + private Long quarterId; - private int keyResultsInFailAmount; - private int keyResultsInCommitAmount; - private int keyResultsInTargetAmount; - private int keyResultsInStretchAmount; + @Enumerated(EnumType.STRING) + private State objectiveState; + private String keyResultType; + private Double baseline; + private Double commitValue; + private Double targetValue; + private Double stretchGoal; + + private Double valueMetric; + @Enumerated(EnumType.STRING) + private Zone zone; + private Instant latestCheckInDate; + + public EvaluationView() { + } private EvaluationView(Builder builder) { - evaluationViewId = builder.evaluationViewId; - objectiveAmount = builder.objectiveAmount; - completedObjectivesAmount = builder.completedObjectivesAmount; - successfullyCompletedObjectivesAmount = builder.successfullyCompletedObjectivesAmount; - keyResultAmount = builder.keyResultAmount; - keyResultsOrdinalAmount = builder.keyResultsOrdinalAmount; - keyResultsMetricAmount = builder.keyResultsMetricAmount; - keyResultsInTargetOrStretchAmount = builder.keyResultsInTargetOrStretchAmount; - keyResultsInFailAmount = builder.keyResultsInFailAmount; - keyResultsInCommitAmount = builder.keyResultsInCommitAmount; - keyResultsInTargetAmount = builder.keyResultsInTargetAmount; - keyResultsInStretchAmount = builder.keyResultsInStretchAmount; + this.rowId = builder.rowId; + this.keyResultId = builder.keyResultId; + this.objectiveId = builder.objectiveId; + this.teamId = builder.teamId; + this.quarterId = builder.quarterId; + this.objectiveState = builder.objectiveState; + this.keyResultType = builder.keyResultType; + this.baseline = builder.baseline; + this.commitValue = builder.commitValue; + this.targetValue = builder.targetValue; + this.stretchGoal = builder.stretchGoal; + this.valueMetric = builder.valueMetric; + this.zone = builder.zone; + this.latestCheckInDate = builder.latestCheckInDate; } - public EvaluationView() { + public Long getRowId() { + return rowId; + } + + public Long getKeyResultId() { + return keyResultId; } - public EvaluationViewId getEvaluationViewId() { - return evaluationViewId; + public Long getObjectiveId() { + return objectiveId; } - public int getObjectiveAmount() { - return objectiveAmount; + public Long getTeamId() { + return teamId; } - public int getCompletedObjectivesAmount() { - return completedObjectivesAmount; + public Long getQuarterId() { + return quarterId; } - public int getSuccessfullyCompletedObjectivesAmount() { - return successfullyCompletedObjectivesAmount; + public State getObjectiveState() { + return objectiveState; } - public int getKeyResultAmount() { - return keyResultAmount; + public String getKeyResultType() { + return keyResultType; } - public int getKeyResultsOrdinalAmount() { - return keyResultsOrdinalAmount; + public Double getBaseline() { + return baseline; } - public int getKeyResultsMetricAmount() { - return keyResultsMetricAmount; + public Double getCommitValue() { + return commitValue; } - public int getKeyResultsInTargetOrStretchAmount() { - return keyResultsInTargetOrStretchAmount; + public Double getTargetValue() { + return targetValue; } - public int getKeyResultsInFailAmount() { - return keyResultsInFailAmount; + public Double getStretchGoal() { + return stretchGoal; } - public int getKeyResultsInCommitAmount() { - return keyResultsInCommitAmount; + public Double getValueMetric() { + return valueMetric; } - public int getKeyResultsInTargetAmount() { - return keyResultsInTargetAmount; + public Zone getZone() { + return zone; } - public int getKeyResultsInStretchAmount() { - return keyResultsInStretchAmount; + public Instant getLatestCheckInDate() { + return latestCheckInDate; } public static final class Builder { - private EvaluationViewId evaluationViewId; - private int objectiveAmount; - private int completedObjectivesAmount; - private int successfullyCompletedObjectivesAmount; - private int keyResultAmount; - private int keyResultsOrdinalAmount; - private int keyResultsMetricAmount; - private int keyResultsInTargetOrStretchAmount; - private int keyResultsInFailAmount; - private int keyResultsInCommitAmount; - private int keyResultsInTargetAmount; - private int keyResultsInStretchAmount; + private Long rowId; + private Long keyResultId; + private Long objectiveId; + private Long teamId; + private Long quarterId; + + @Enumerated(EnumType.STRING) + private State objectiveState; + private String keyResultType; + private Double baseline; + private Double commitValue; + private Double targetValue; + private Double stretchGoal; + + private Double valueMetric; + @Enumerated(EnumType.STRING) + private Zone zone; + private Instant latestCheckInDate; private Builder() { } @@ -112,63 +138,73 @@ public static Builder builder() { return new Builder(); } - public Builder withEvaluationViewId(EvaluationViewId val) { - evaluationViewId = val; + public Builder withRowId(Long val) { + this.rowId = val; + return this; + } + + public Builder withKeyResultId(Long val) { + this.keyResultId = val; + return this; + } + + public Builder withObjectiveId(Long val) { + this.objectiveId = val; return this; } - public Builder withObjectiveAmount(int val) { - objectiveAmount = val; + public Builder withTeamId(Long val) { + this.teamId = val; return this; } - public Builder withCompletedObjectivesAmount(int val) { - completedObjectivesAmount = val; + public Builder withQuarterId(Long val) { + this.quarterId = val; return this; } - public Builder withSuccessfullyCompletedObjectivesAmount(int val) { - successfullyCompletedObjectivesAmount = val; + public Builder withObjectiveState(State val) { + this.objectiveState = val; return this; } - public Builder withKeyResultAmount(int val) { - keyResultAmount = val; + public Builder withKeyResultType(String val) { + this.keyResultType = val; return this; } - public Builder withKeyResultsOrdinalAmount(int val) { - keyResultsOrdinalAmount = val; + public Builder withBaseline(Double val) { + this.baseline = val; return this; } - public Builder withKeyResultsMetricAmount(int val) { - keyResultsMetricAmount = val; + public Builder withCommitValue(Double val) { + this.commitValue = val; return this; } - public Builder withKeyResultsInTargetOrStretchAmount(int val) { - keyResultsInTargetOrStretchAmount = val; + public Builder withTargetValue(Double val) { + this.targetValue = val; return this; } - public Builder withKeyResultsInFailAmount(int val) { - keyResultsInFailAmount = val; + public Builder withStretchGoal(Double val) { + this.stretchGoal = val; return this; } - public Builder withKeyResultsInCommitAmount(int val) { - keyResultsInCommitAmount = val; + public Builder withValueMetric(Double val) { + this.valueMetric = val; return this; } - public Builder withKeyResultsInTargetAmount(int val) { - keyResultsInTargetAmount = val; + public Builder withZone(Zone val) { + this.zone = val; return this; } - public Builder withKeyResultsInStretchAmount(int val) { - keyResultsInStretchAmount = val; + public Builder withLatestCheckInDate(Instant val) { + this.latestCheckInDate = val; return this; } @@ -182,34 +218,35 @@ public boolean equals(Object o) { if (!(o instanceof EvaluationView that)) { return false; } - return getObjectiveAmount() == that.getObjectiveAmount() - && getCompletedObjectivesAmount() == that.getCompletedObjectivesAmount() - && getSuccessfullyCompletedObjectivesAmount() == that.getSuccessfullyCompletedObjectivesAmount() - && getKeyResultAmount() == that.getKeyResultAmount() - && getKeyResultsOrdinalAmount() == that.getKeyResultsOrdinalAmount() - && getKeyResultsMetricAmount() == that.getKeyResultsMetricAmount() - && getKeyResultsInTargetOrStretchAmount() == that.getKeyResultsInTargetOrStretchAmount() - && getKeyResultsInFailAmount() == that.getKeyResultsInFailAmount() - && getKeyResultsInCommitAmount() == that.getKeyResultsInCommitAmount() - && getKeyResultsInTargetAmount() == that.getKeyResultsInTargetAmount() - && getKeyResultsInStretchAmount() == that.getKeyResultsInStretchAmount() - && Objects.equals(getEvaluationViewId(), that.getEvaluationViewId()); + return Objects.equals(getRowId(), that.getRowId()) && Objects.equals(getKeyResultId(), that.getKeyResultId()) + && Objects.equals(getObjectiveId(), that.getObjectiveId()) + && Objects.equals(getTeamId(), that.getTeamId()) && Objects.equals(getQuarterId(), that.getQuarterId()) + && Objects.equals(getObjectiveState(), that.getObjectiveState()) + && Objects.equals(getKeyResultType(), that.getKeyResultType()) + && Objects.equals(getBaseline(), that.getBaseline()) + && Objects.equals(getCommitValue(), that.getCommitValue()) + && Objects.equals(getTargetValue(), that.getTargetValue()) + && Objects.equals(getStretchGoal(), that.getStretchGoal()) + && Objects.equals(getValueMetric(), that.getValueMetric()) && Objects.equals(getZone(), that.getZone()) + && Objects.equals(getLatestCheckInDate(), that.getLatestCheckInDate()); } @Override public int hashCode() { return Objects - .hash(getEvaluationViewId(), - getObjectiveAmount(), - getCompletedObjectivesAmount(), - getSuccessfullyCompletedObjectivesAmount(), - getKeyResultAmount(), - getKeyResultsOrdinalAmount(), - getKeyResultsMetricAmount(), - getKeyResultsInTargetOrStretchAmount(), - getKeyResultsInFailAmount(), - getKeyResultsInCommitAmount(), - getKeyResultsInTargetAmount(), - getKeyResultsInStretchAmount()); + .hash(getRowId(), + getKeyResultId(), + getObjectiveId(), + getTeamId(), + getQuarterId(), + getObjectiveState(), + getKeyResultType(), + getBaseline(), + getCommitValue(), + getTargetValue(), + getStretchGoal(), + getValueMetric(), + getZone(), + getLatestCheckInDate()); } } diff --git a/backend/src/main/java/ch/puzzle/okr/models/evaluation/EvaluationViewId.java b/backend/src/main/java/ch/puzzle/okr/models/evaluation/EvaluationViewId.java deleted file mode 100644 index 402db5f413..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/models/evaluation/EvaluationViewId.java +++ /dev/null @@ -1,71 +0,0 @@ -package ch.puzzle.okr.models.evaluation; - -import jakarta.persistence.Embeddable; -import java.io.Serializable; -import java.util.Objects; - -@Embeddable -public class EvaluationViewId implements Serializable { - private Long teamId; - private Long quarterId; - - public EvaluationViewId(Long teamId, Long quarterId) { - this.teamId = teamId; - this.quarterId = quarterId; - } - - public EvaluationViewId() { - } - - private EvaluationViewId(Builder builder) { - teamId = builder.teamId; - quarterId = builder.quarterId; - } - - public Long getTeamId() { - return teamId; - } - - public Long getQuarterId() { - return quarterId; - } - - public static final class Builder { - private Long teamId; - private Long quarterId; - - private Builder() { - } - - public static Builder builder() { - return new Builder(); - } - - public Builder withTeamId(Long val) { - teamId = val; - return this; - } - - public Builder withQuarterId(Long val) { - quarterId = val; - return this; - } - - public EvaluationViewId build() { - return new EvaluationViewId(this); - } - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof EvaluationViewId that)) { - return false; - } - return Objects.equals(getTeamId(), that.getTeamId()) && Objects.equals(getQuarterId(), that.getQuarterId()); - } - - @Override - public int hashCode() { - return Objects.hash(getTeamId(), getQuarterId()); - } -} diff --git a/backend/src/main/java/ch/puzzle/okr/repository/EvaluationViewRepository.java b/backend/src/main/java/ch/puzzle/okr/repository/EvaluationViewRepository.java index 755985b24e..d824d5d256 100644 --- a/backend/src/main/java/ch/puzzle/okr/repository/EvaluationViewRepository.java +++ b/backend/src/main/java/ch/puzzle/okr/repository/EvaluationViewRepository.java @@ -1,8 +1,11 @@ package ch.puzzle.okr.repository; import ch.puzzle.okr.models.evaluation.EvaluationView; -import ch.puzzle.okr.models.evaluation.EvaluationViewId; +import java.sql.RowId; +import java.util.List; import org.springframework.data.repository.CrudRepository; -public interface EvaluationViewRepository extends CrudRepository { +public interface EvaluationViewRepository extends CrudRepository { + + List findByTeamIdInAndQuarterId(List teamIds, Long quarterId); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/EvaluationViewBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/EvaluationViewBusinessService.java index 0ba1089efc..d748f13d51 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/EvaluationViewBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/EvaluationViewBusinessService.java @@ -1,14 +1,20 @@ package ch.puzzle.okr.service.business; +import ch.puzzle.okr.models.State; +import ch.puzzle.okr.models.checkin.Zone; import ch.puzzle.okr.models.evaluation.EvaluationView; -import ch.puzzle.okr.models.evaluation.EvaluationViewId; import ch.puzzle.okr.service.persistence.EvaluationViewPersistenceService; import ch.puzzle.okr.service.validation.EvaluationViewValidationService; +import ch.puzzle.okr.util.TeamQuarterFilter; import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; import org.springframework.stereotype.Service; @Service public class EvaluationViewBusinessService { + private final EvaluationViewPersistenceService evaluationViewPersistenceService; private final EvaluationViewValidationService evaluationViewValidationService; @@ -18,8 +24,123 @@ public EvaluationViewBusinessService(EvaluationViewPersistenceService evaluation this.evaluationViewValidationService = evaluationViewValidationService; } - public List findByIds(List ids) { - evaluationViewValidationService.validateOnGet(ids); - return evaluationViewPersistenceService.findByIds(ids); + public List findByIds(TeamQuarterFilter filter) { + evaluationViewValidationService.validateOnGet(filter); + return evaluationViewPersistenceService.findByIds(filter); + } + + public long calculateObjectiveSum(List views) { + return views.stream().map(EvaluationView::getObjectiveId).distinct().count(); + } + + public long calculateCompletedObjectivesSum(List views) { + return views + .stream() + .filter(Predicate.not(v -> State.ONGOING.equals(v.getObjectiveState()))) + .map(EvaluationView::getObjectiveId) + .distinct() + .count(); + } + + public long calculateSuccessfullyCompletedObjectivesSum(List views) { + return views + .stream() + .filter(v -> State.SUCCESSFUL.equals(v.getObjectiveState())) + .map(EvaluationView::getObjectiveId) + .distinct() + .count(); + } + + public long calculateKeyResultSum(List views) { + return views.stream().map(EvaluationView::getKeyResultId).filter(Objects::nonNull).distinct().count(); + } + + public long calculateKeyResultsOrdinalSum(List views) { + return views.stream().filter(v -> "ordinal".equalsIgnoreCase(v.getKeyResultType())).count(); + } + + public long calculateKeyResultsMetricSum(List views) { + return views.stream().filter(v -> "metric".equalsIgnoreCase(v.getKeyResultType())).count(); + } + + public long calculateKeyResultsInTargetOrStretchSum(List views) { + return views.stream().filter(this::isKeyResultInTargetOrStretch).count(); + } + + public long calculateKeyResultsInFailSum(List views) { + return views.stream().filter(this::isKeyResultInFail).count(); + } + + public long calculateKeyResultsInCommitSum(List views) { + return views.stream().filter(this::isKeyResultInCommit).count(); + } + + public long calculateKeyResultsInTargetSum(List views) { + return views.stream().filter(this::isKeyResultInTarget).count(); + } + + public long calculateKeyResultsInStretchSum(List views) { + return views.stream().filter(this::isKeyResultInStretch).count(); + } + + private boolean isKeyResultInTargetOrStretch(EvaluationView v) { + if ("ordinal".equalsIgnoreCase(v.getKeyResultType())) { + Zone zone = v.getZone(); + return zone == Zone.TARGET || zone == Zone.STRETCH; + } else if ("metric".equalsIgnoreCase(v.getKeyResultType())) { + return calculateProgressRatio(v).map(progress -> progress >= 0.7).orElse(false); + } + return false; + } + + private boolean isKeyResultInFail(EvaluationView v) { + if ("ordinal".equalsIgnoreCase(v.getKeyResultType())) { + Zone zone = v.getZone(); + return zone == Zone.FAIL; + } else if ("metric".equalsIgnoreCase(v.getKeyResultType())) { + return calculateProgressRatio(v).map(progress -> progress < 0.3).orElse(false); + } + return false; + } + + private boolean isKeyResultInCommit(EvaluationView v) { + if ("ordinal".equalsIgnoreCase(v.getKeyResultType())) { + Zone zone = v.getZone(); + return zone == Zone.COMMIT; + } else if ("metric".equalsIgnoreCase(v.getKeyResultType())) { + return calculateProgressRatio(v).map(progress -> progress >= 0.3 && progress < 0.7).orElse(false); + } + return false; + } + + private boolean isKeyResultInTarget(EvaluationView v) { + if ("ordinal".equalsIgnoreCase(v.getKeyResultType())) { + Zone zone = v.getZone(); + return zone == Zone.TARGET; + } else if ("metric".equalsIgnoreCase(v.getKeyResultType())) { + return calculateProgressRatio(v).map(progress -> progress >= 0.7 && progress < 1.0).orElse(false); + } + return false; + } + + private boolean isKeyResultInStretch(EvaluationView v) { + if ("ordinal".equalsIgnoreCase(v.getKeyResultType())) { + Zone zone = v.getZone(); + return zone == Zone.STRETCH; + } else if ("metric".equalsIgnoreCase(v.getKeyResultType())) { + return calculateProgressRatio(v).map(progress -> progress >= 1.0).orElse(false); + } + return false; + } + + private Optional calculateProgressRatio(EvaluationView v) { + if (v.getBaseline() == null || v.getStretchGoal() == null || v.getValueMetric() == null) { + return Optional.empty(); + } + double denominator = v.getStretchGoal() - v.getBaseline(); + if (denominator == 0) { + return Optional.empty(); + } + return Optional.of((v.getValueMetric() - v.getBaseline()) / denominator); } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/EvaluationViewPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/EvaluationViewPersistenceService.java index 28bf6562fa..e6d9b76756 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/EvaluationViewPersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/EvaluationViewPersistenceService.java @@ -1,22 +1,21 @@ package ch.puzzle.okr.service.persistence; import ch.puzzle.okr.models.evaluation.EvaluationView; -import ch.puzzle.okr.models.evaluation.EvaluationViewId; import ch.puzzle.okr.repository.EvaluationViewRepository; +import ch.puzzle.okr.util.TeamQuarterFilter; +import java.sql.RowId; import java.util.List; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Service; @Service -public class EvaluationViewPersistenceService - extends - PersistenceBase { - protected EvaluationViewPersistenceService(CrudRepository repository) { +public class EvaluationViewPersistenceService extends PersistenceBase { + protected EvaluationViewPersistenceService(CrudRepository repository) { super(repository); } - public List findByIds(List ids) { - return iteratorToList(getRepository().findAllById(ids)); + public List findByIds(TeamQuarterFilter filter) { + return getRepository().findByTeamIdInAndQuarterId(filter.teamIds(), filter.quarterId()); } @Override diff --git a/backend/src/main/java/ch/puzzle/okr/service/validation/EvaluationViewValidationService.java b/backend/src/main/java/ch/puzzle/okr/service/validation/EvaluationViewValidationService.java index 188054b6cf..851381238a 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/validation/EvaluationViewValidationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/validation/EvaluationViewValidationService.java @@ -2,17 +2,17 @@ import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.evaluation.EvaluationView; -import ch.puzzle.okr.models.evaluation.EvaluationViewId; import ch.puzzle.okr.repository.EvaluationViewRepository; import ch.puzzle.okr.service.persistence.EvaluationViewPersistenceService; -import java.util.List; +import ch.puzzle.okr.util.TeamQuarterFilter; +import java.sql.RowId; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @Service public class EvaluationViewValidationService extends - ValidationBase { + ValidationBase { private final QuarterValidationService quarterValidationService; private final TeamValidationService teamValidationService; @@ -25,12 +25,13 @@ public class EvaluationViewValidationService this.teamValidationService = teamValidationService; } - public void validateOnGet(List ids) { - if (ids.isEmpty()) { - throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, "Es muss mindestens 1 Team angewählt werden"); + public void validateOnGet(TeamQuarterFilter filter) { + if (filter.quarterId() == null || filter.teamIds().isEmpty()) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, + "Es muss mindestens 1 Team und 1 Quartal ausgewählt sein"); } - ids.forEach(id -> teamValidationService.validateOnGet(id.getTeamId())); - quarterValidationService.validateOnGet(ids.getLast().getQuarterId()); + filter.teamIds().forEach(teamValidationService::validateOnGet); + quarterValidationService.validateOnGet(filter.quarterId()); } @Override @@ -39,7 +40,7 @@ public void validateOnCreate(EvaluationView model) { } @Override - public void validateOnUpdate(EvaluationViewId evaluationViewId, EvaluationView model) { + public void validateOnUpdate(RowId id, EvaluationView model) { throw new UnsupportedOperationException("EvaluationView is for get Operations only."); } } diff --git a/backend/src/main/java/ch/puzzle/okr/util/TeamQuarterFilter.java b/backend/src/main/java/ch/puzzle/okr/util/TeamQuarterFilter.java new file mode 100644 index 0000000000..bca4d4effe --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/util/TeamQuarterFilter.java @@ -0,0 +1,6 @@ +package ch.puzzle.okr.util; + +import java.util.List; + +public record TeamQuarterFilter(List teamIds, Long quarterId) { +} diff --git a/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql b/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql index 921dade574..113c22feab 100644 --- a/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql +++ b/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql @@ -281,95 +281,42 @@ ALTER TABLE IF EXISTS person_team CREATE SEQUENCE IF NOT EXISTS sequence_person_team; DROP VIEW IF EXISTS EVALUATION_VIEW; + CREATE VIEW EVALUATION_VIEW AS -WITH team_quarters AS ( - SELECT - t.id AS team_id, - t.name AS team_name, - q.id AS quarter_id, - q.label AS quarter_label - FROM team t - CROSS JOIN quarter q -), - objectives AS ( - SELECT - o.team_id, - o.quarter_id, - COUNT(DISTINCT o.id) AS objective_amount, - COUNT(*) FILTER (WHERE o.state IN ('SUCCESSFUL', 'NOTSUCCESSFUL')) AS completed_objectives_amount, - COUNT(*) FILTER (WHERE o.state = 'SUCCESSFUL') AS successfully_completed_objectives_amount - FROM OBJECTIVE o - GROUP BY o.team_id, o.quarter_id - ), - kr_latest_check_in AS ( - SELECT DISTINCT ON (kr.id) - kr.id as key_result_id, - ci.value_metric, - COALESCE(((value_metric - baseline) / NULLIF(stretch_goal - baseline, 0)),0) as progress, - ci.zone, - kr.key_result_type, - kr.stretch_goal, - kr.baseline, - sub_o.team_id, - sub_o.quarter_id - FROM key_result kr - LEFT JOIN CHECK_IN ci ON KR.ID = ci.KEY_RESULT_ID AND ci.MODIFIED_ON = (SELECT MAX(CC.MODIFIED_ON) - FROM CHECK_IN CC - WHERE CC.KEY_RESULT_ID = ci.KEY_RESULT_ID) - INNER JOIN objective sub_o ON kr.objective_id = sub_o.id - ORDER BY kr.id - ), - key_result_counts AS ( - SELECT - team_id, - quarter_id, - COUNT(*) AS key_result_amount, - COUNT(*) FILTER (WHERE key_result_type = 'ordinal') AS key_results_ordinal_amount, - COUNT(*) FILTER (WHERE key_result_type = 'metric') AS key_results_metric_amount, - COUNT(*) FILTER ( - WHERE (key_result_type = 'ordinal' AND zone IN ('TARGET', 'STRETCH')) - OR (key_result_type = 'metric' AND progress >= 0.7) - ) AS key_results_in_target_or_stretch_amount, - COUNT(*) FILTER ( - WHERE (key_result_type = 'ordinal' AND zone = 'FAIL') - OR (key_result_type = 'metric' AND progress > 0 AND progress < 0.3) - ) AS key_results_in_fail_amount, - COUNT(*) FILTER ( - WHERE (key_result_type = 'ordinal' AND zone = 'COMMIT') - OR (key_result_type = 'metric' AND progress >= 0.3 AND progress < 0.7) - ) AS key_results_in_commit_amount, - COUNT(*) FILTER ( - WHERE (key_result_type = 'ordinal' AND zone = 'TARGET') - OR (key_result_type = 'metric' AND progress >= 0.7 AND progress < 1) - ) AS key_results_in_target_amount, - COUNT(*) FILTER ( - WHERE (key_result_type = 'ordinal' AND zone = 'STRETCH') - OR (key_result_type = 'metric' AND progress >= 1) - ) AS key_results_in_stretch_amount - FROM kr_latest_check_in - GROUP BY team_id, quarter_id - ) SELECT - tq.team_id, - tq.team_name, - tq.quarter_id, - tq.quarter_label, - COALESCE(o.objective_amount, 0) AS objective_amount, - COALESCE(o.completed_objectives_amount , 0) as completed_objectives_amount, - COALESCE(o.successfully_completed_objectives_amount, 0) as successfully_completed_objectives_amount, - COALESCE(kr.key_result_amount, 0) as key_result_amount, - COALESCE(kr.key_results_ordinal_amount, 0) as key_results_ordinal_amount, - COALESCE(kr.key_results_metric_amount, 0) as key_results_metric_amount, - COALESCE(kr.key_results_in_target_or_stretch_amount, 0) as key_results_in_target_or_stretch_amount, - COALESCE(kr.key_results_in_fail_amount, 0) as key_results_in_fail_amount, - COALESCE(kr.key_results_in_commit_amount, 0) as key_results_in_commit_amount, - COALESCE(kr.key_results_in_target_amount, 0) as key_results_in_target_amount, - COALESCE(kr.key_results_in_stretch_amount, 0) as key_results_in_stretch_amount -FROM team_quarters tq - LEFT JOIN objectives o - ON tq.team_id = o.team_id - AND tq.quarter_id = o.quarter_id - LEFT JOIN key_result_counts kr - ON tq.team_id = kr.team_id - AND tq.quarter_id = kr.quarter_id -order by tq.team_id, tq.quarter_id; + row_number() OVER () AS row_id, + o.id AS objective_id, + o.team_id, + o.quarter_id, + o.state AS objective_state, + kr.id AS key_result_id, + kr.key_result_type, + kr.baseline, + kr.commit_value, + kr.target_value, + kr.stretch_goal, + ( + SELECT ci2.value_metric + FROM check_in ci2 + WHERE ci2.key_result_id = kr.id + ORDER BY ci2.modified_on DESC + LIMIT 1 + ) AS value_metric, + ( + SELECT ci2.zone + FROM check_in ci2 + WHERE ci2.key_result_id = kr.id + ORDER BY ci2.modified_on DESC + LIMIT 1 + ) AS zone, + ( + SELECT ci2.modified_on + FROM check_in ci2 + WHERE ci2.key_result_id = kr.id + ORDER BY ci2.modified_on DESC + LIMIT 1 + ) AS latest_check_in_date +FROM objective o + LEFT JOIN key_result kr ON kr.objective_id = o.id +WHERE o.state <> 'DRAFT'; + diff --git a/backend/src/main/resources/db/migration/V3_6_3__recreate_evaluation_view.sql b/backend/src/main/resources/db/migration/V3_6_3__recreate_evaluation_view.sql new file mode 100644 index 0000000000..a2f4091361 --- /dev/null +++ b/backend/src/main/resources/db/migration/V3_6_3__recreate_evaluation_view.sql @@ -0,0 +1,32 @@ +DROP VIEW IF EXISTS EVALUATION_VIEW; + +CREATE VIEW EVALUATION_VIEW AS +SELECT + row_number() OVER () AS row_id, + o.id AS objective_id, + o.team_id, + o.quarter_id, + o.state AS objective_state, + kr.id AS key_result_id, + kr.key_result_type, + kr.baseline, + kr.commit_value, + kr.target_value, + kr.stretch_goal, + ci.value_metric, + CASE + WHEN kr.key_result_type = 'ORDINAL' THEN NULLIF(ci.zone, '') + ELSE NULL + END AS zone, + ci.modified_on AS latest_check_in_date +FROM objective o + LEFT JOIN key_result kr + ON kr.objective_id = o.id + LEFT JOIN LATERAL ( + SELECT ci2.* + FROM check_in ci2 + WHERE ci2.key_result_id = kr.id + ORDER BY ci2.modified_on DESC + LIMIT 1 + ) ci ON TRUE +WHERE o.state <> 'DRAFT'; \ No newline at end of file diff --git a/backend/src/test/java/ch/puzzle/okr/controller/EvaluationViewControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/EvaluationViewControllerIT.java index 608f0826fa..044e2b22c7 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/EvaluationViewControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/EvaluationViewControllerIT.java @@ -5,8 +5,8 @@ import ch.puzzle.okr.dto.EvaluationDto; import ch.puzzle.okr.mapper.EvaluationViewMapper; import ch.puzzle.okr.models.evaluation.EvaluationView; -import ch.puzzle.okr.models.evaluation.EvaluationViewId; import ch.puzzle.okr.service.business.EvaluationViewBusinessService; +import ch.puzzle.okr.util.TeamQuarterFilter; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -42,15 +42,15 @@ class EvaluationViewControllerIT { void shouldReturnEvaluationData() throws Exception { List teamIds = List.of(1L, 2L); Long quarterId = 3L; - List evaluationViewIds = getEvaluationViewIds(teamIds, quarterId); - // Dummy objects to simulate the internal workflow - List evaluationViews = generateEvaluationViews(evaluationViewIds); + TeamQuarterFilter teamQuarterFilter = new TeamQuarterFilter(teamIds, quarterId); + // Dummy object to simulate the internal workflow + List evaluationViews = generateEvaluationViews(teamQuarterFilter); // Create a dummy EvaluationDto with sample data EvaluationDto evaluationDto = generateEvaluationDto(); // Define mock behavior - BDDMockito.given(evaluationViewMapper.fromDto(teamIds, quarterId)).willReturn(evaluationViewIds); - BDDMockito.given(evaluationViewBusinessService.findByIds(evaluationViewIds)).willReturn(evaluationViews); + BDDMockito.given(evaluationViewMapper.fromDto(teamIds, quarterId)).willReturn(teamQuarterFilter); + BDDMockito.given(evaluationViewBusinessService.findByIds(teamQuarterFilter)).willReturn(evaluationViews); BDDMockito.given(evaluationViewMapper.toDto(evaluationViews)).willReturn(evaluationDto); // Perform GET request and assert the JSON response @@ -103,12 +103,12 @@ void shouldReturnNotFoundForNonExistentIds() throws Exception { List teamIds = List.of(999L); Long quarterId = 888L; - List evaluationViewIds = getEvaluationViewIds(teamIds, quarterId); + TeamQuarterFilter teamQuarterFilter = new TeamQuarterFilter(teamIds, quarterId); - BDDMockito.given(evaluationViewMapper.fromDto(teamIds, quarterId)).willReturn(evaluationViewIds); + BDDMockito.given(evaluationViewMapper.fromDto(teamIds, quarterId)).willReturn(teamQuarterFilter); // Simulate not found by throwing an exception BDDMockito - .given(evaluationViewBusinessService.findByIds(evaluationViewIds)) + .given(evaluationViewBusinessService.findByIds(teamQuarterFilter)) .willThrow(new ResponseStatusException(HttpStatus.NOT_FOUND)); mvc @@ -127,11 +127,11 @@ void shouldReturnUnauthorizedForUnauthenticated() throws Exception { List teamIds = List.of(999L); Long quarterId = 888L; - List evaluationViewIds = getEvaluationViewIds(teamIds, quarterId); + TeamQuarterFilter teamQuarterFilter = new TeamQuarterFilter(teamIds, quarterId); - BDDMockito.given(evaluationViewMapper.fromDto(teamIds, quarterId)).willReturn(evaluationViewIds); + BDDMockito.given(evaluationViewMapper.fromDto(teamIds, quarterId)).willReturn(teamQuarterFilter); BDDMockito - .given(evaluationViewBusinessService.findByIds(evaluationViewIds)) + .given(evaluationViewBusinessService.findByIds(teamQuarterFilter)) .willThrow(new ResponseStatusException(HttpStatus.UNAUTHORIZED)); mvc .perform(MockMvcRequestBuilders diff --git a/backend/src/test/java/ch/puzzle/okr/mapper/EvaluationViewMapperTest.java b/backend/src/test/java/ch/puzzle/okr/mapper/EvaluationViewMapperTest.java index 3fb55466e3..5af6083487 100644 --- a/backend/src/test/java/ch/puzzle/okr/mapper/EvaluationViewMapperTest.java +++ b/backend/src/test/java/ch/puzzle/okr/mapper/EvaluationViewMapperTest.java @@ -1,76 +1,73 @@ package ch.puzzle.okr.mapper; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; import ch.puzzle.okr.dto.EvaluationDto; import ch.puzzle.okr.models.evaluation.EvaluationView; -import ch.puzzle.okr.models.evaluation.EvaluationViewId; -import ch.puzzle.okr.test.EvaluationViewTestHelper; -import java.util.List; -import java.util.stream.Stream; +import ch.puzzle.okr.service.business.EvaluationViewBusinessService; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InjectMocks; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class EvaluationViewMapperTest { + @InjectMocks private EvaluationViewMapper mapper; + private EvaluationViewBusinessService businessService; - private static Stream fromDtoArgs() { - return Stream - .of(Arguments - .of(List.of(1L, 2L, 3L), - 3L, - List - .of(new EvaluationViewId(1L, 3L), - new EvaluationViewId(2L, 3L), - new EvaluationViewId(3L, 3L))), - Arguments.of(List.of(), 3L, List.of()), - Arguments.of(List.of(7L), 5L, List.of(new EvaluationViewId(7L, 5L))), - Arguments - .of(List.of(10L, 20L), - 2L, - List.of(new EvaluationViewId(10L, 2L), new EvaluationViewId(20L, 2L)))); + @BeforeEach + void setUp() { + businessService = mock(EvaluationViewBusinessService.class); + mapper = new EvaluationViewMapper(businessService); } - private static Stream toDtoArge() { - EvaluationViewId id = new EvaluationViewId(1L, 3L); - List data1 = List.of(1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21); - List data2 = List.of(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20); - List data3 = List.of(1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31); - EvaluationView evaluationView1 = EvaluationViewTestHelper.createEvaluationView(id, data1); - EvaluationView evaluationView2 = EvaluationViewTestHelper.createEvaluationView(id, data2); - EvaluationView evaluationView3 = EvaluationViewTestHelper.createEvaluationView(id, data3); - - EvaluationDto evaluationDto1 = EvaluationViewTestHelper.generateEvaluationDto(data1); + @DisplayName("toDto maps values from EvaluationViewBusinessService correctly into EvaluationDto") + @Test + void toDto_shouldMapValuesFromBusinessService() { + var views = Collections.singletonList(new EvaluationView()); - List evaluationDtoData12 = List.of(1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41); - EvaluationDto evaluationDto12 = EvaluationViewTestHelper.generateEvaluationDto(evaluationDtoData12); + when(businessService.calculateObjectiveSum(views)).thenReturn(1L); + when(businessService.calculateCompletedObjectivesSum(views)).thenReturn(2L); + when(businessService.calculateSuccessfullyCompletedObjectivesSum(views)).thenReturn(3L); + when(businessService.calculateKeyResultSum(views)).thenReturn(4L); + when(businessService.calculateKeyResultsOrdinalSum(views)).thenReturn(5L); + when(businessService.calculateKeyResultsMetricSum(views)).thenReturn(6L); + when(businessService.calculateKeyResultsInTargetOrStretchSum(views)).thenReturn(7L); + when(businessService.calculateKeyResultsInFailSum(views)).thenReturn(8L); + when(businessService.calculateKeyResultsInCommitSum(views)).thenReturn(9L); + when(businessService.calculateKeyResultsInTargetSum(views)).thenReturn(10L); + when(businessService.calculateKeyResultsInStretchSum(views)).thenReturn(11L); - List evaluationDtoData123 = List.of(2, 9, 16, 23, 30, 37, 44, 51, 58, 65, 72); - EvaluationDto evaluationDto123 = EvaluationViewTestHelper.generateEvaluationDto(evaluationDtoData123); - return Stream - .of(Arguments.of(evaluationDto1, List.of(evaluationView1)), - Arguments.of(evaluationDto12, List.of(evaluationView1, evaluationView2)), - Arguments.of(evaluationDto123, List.of(evaluationView1, evaluationView2, evaluationView3))); - } + EvaluationDto dto = mapper.toDto(views); - @ParameterizedTest - @MethodSource("fromDtoArgs") - void shouldMapTeamsIdsAndQuarterToEvaluationViewIds(List teamIds, Long quarterId, - List evaluationViewIds) { - var result = mapper.fromDto(teamIds, quarterId); - assertEquals(evaluationViewIds, result); - } + assertThat(dto.objectiveAmount()).isEqualTo(1); + assertThat(dto.completedObjectivesAmount()).isEqualTo(2); + assertThat(dto.successfullyCompletedObjectivesAmount()).isEqualTo(3); + assertThat(dto.keyResultAmount()).isEqualTo(4); + assertThat(dto.keyResultsOrdinalAmount()).isEqualTo(5); + assertThat(dto.keyResultsMetricAmount()).isEqualTo(6); + assertThat(dto.keyResultsInTargetOrStretchAmount()).isEqualTo(7); + assertThat(dto.keyResultsInFailAmount()).isEqualTo(8); + assertThat(dto.keyResultsInCommitAmount()).isEqualTo(9); + assertThat(dto.keyResultsInTargetAmount()).isEqualTo(10); + assertThat(dto.keyResultsInStretchAmount()).isEqualTo(11); - @ParameterizedTest - @MethodSource("toDtoArge") - void shouldEvaluationViewsToDto(EvaluationDto dto, List entities) { - var result = mapper.toDto(entities); - assertEquals(dto, result); + verify(businessService).calculateObjectiveSum(views); + verify(businessService).calculateCompletedObjectivesSum(views); + verify(businessService).calculateSuccessfullyCompletedObjectivesSum(views); + verify(businessService).calculateKeyResultSum(views); + verify(businessService).calculateKeyResultsOrdinalSum(views); + verify(businessService).calculateKeyResultsMetricSum(views); + verify(businessService).calculateKeyResultsInTargetOrStretchSum(views); + verify(businessService).calculateKeyResultsInFailSum(views); + verify(businessService).calculateKeyResultsInCommitSum(views); + verify(businessService).calculateKeyResultsInTargetSum(views); + verify(businessService).calculateKeyResultsInStretchSum(views); } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/EvaluationViewBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/EvaluationViewBusinessServiceTest.java index 8807836d99..d028232a28 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/EvaluationViewBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/EvaluationViewBusinessServiceTest.java @@ -1,12 +1,16 @@ package ch.puzzle.okr.service.business; -import static org.mockito.ArgumentMatchers.anyList; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import ch.puzzle.okr.models.evaluation.EvaluationViewId; +import ch.puzzle.okr.models.State; +import ch.puzzle.okr.models.checkin.Zone; +import ch.puzzle.okr.models.evaluation.EvaluationView; import ch.puzzle.okr.service.persistence.EvaluationViewPersistenceService; import ch.puzzle.okr.service.validation.EvaluationViewValidationService; +import ch.puzzle.okr.util.TeamQuarterFilter; +import java.time.Instant; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -24,12 +28,118 @@ class EvaluationViewBusinessServiceTest { @InjectMocks private EvaluationViewBusinessService evaluationViewBusinessService; - @DisplayName("Should validate method calls on get by ids") + @DisplayName("Should validate method call on findByIds") @Test void shouldGetAction() { - List ids = List.of(new EvaluationViewId(1L, 1L), new EvaluationViewId(2L, 1L)); + + List teamIds = List.of(1L, 2L); + Long quarterId = 1L; + + TeamQuarterFilter ids = new TeamQuarterFilter(teamIds, quarterId); + evaluationViewBusinessService.findByIds(ids); - verify(evaluationViewPersistenceService, times(1)).findByIds(anyList()); - verify(evaluationViewValidationService, times(1)).validateOnGet(anyList()); + verify(evaluationViewPersistenceService, times(1)).findByIds(ids); + verify(evaluationViewValidationService, times(1)).validateOnGet(ids); + } + + @DisplayName("Should calculate the correct sum of unique objectives") + @Test + void testCalculateObjectiveSum() { + assertEquals(4, evaluationViewBusinessService.calculateObjectiveSum(evaluationViewList)); + } + + @DisplayName("Should calculate the correct sum of completed objectives") + @Test + void testCalculateCompletedObjectivesSum() { + assertEquals(2, evaluationViewBusinessService.calculateCompletedObjectivesSum(evaluationViewList)); + } + + @DisplayName("Should calculate the correct sum of successfully completed objectives") + @Test + void testCalculateSuccessfullyCompletedObjectivesSum() { + assertEquals(1, evaluationViewBusinessService.calculateSuccessfullyCompletedObjectivesSum(evaluationViewList)); + } + + @DisplayName("Should calculate the correct sum of key results") + @Test + void testCalculateKeyResultSum() { + assertEquals(10, evaluationViewBusinessService.calculateKeyResultSum(evaluationViewList)); } + + @DisplayName("Should calculate the correct sum of ordinal key results") + @Test + void testCalculateKeyResultsOrdinalSum() { + assertEquals(4, evaluationViewBusinessService.calculateKeyResultsOrdinalSum(evaluationViewList)); + } + + @DisplayName("Should calculate the correct sum of metric key results") + @Test + void testCalculateKeyResultsMetricSum() { + assertEquals(5, evaluationViewBusinessService.calculateKeyResultsMetricSum(evaluationViewList)); + } + + @DisplayName("Should calculate the correct sum of key results in Target or Stretch zone") + @Test + void testCalculateKeyResultsInTargetOrStretchSum() { + assertEquals(4, evaluationViewBusinessService.calculateKeyResultsInTargetOrStretchSum(evaluationViewList)); + } + + @DisplayName("Should calculate the correct sum of key results in Fail zone") + @Test + void testCalculateKeyResultsInFailSum() { + assertEquals(2, evaluationViewBusinessService.calculateKeyResultsInFailSum(evaluationViewList)); + } + + @DisplayName("Should calculate the correct sum of key results in Commit zone") + @Test + void testCalculateKeyResultsInCommitSum() { + assertEquals(2, evaluationViewBusinessService.calculateKeyResultsInCommitSum(evaluationViewList)); + } + + @DisplayName("Should calculate the correct sum of key results in Target zone") + @Test + void testCalculateKeyResultsInTargetSum() { + assertEquals(2, evaluationViewBusinessService.calculateKeyResultsInTargetSum(evaluationViewList)); + } + + @DisplayName("Should calculate the correct sum of key results in Stretch zone") + @Test + void testCalculateKeyResultsInStretchSum() { + assertEquals(2, evaluationViewBusinessService.calculateKeyResultsInStretchSum(evaluationViewList)); + } + + private static EvaluationView createEvaluationView(Long rowId, Long keyResultId, Long objectiveId, + State objectiveSate, String keyResultType, Double baseline, + Double commitValue, Double targetValue, Double stretchGoal, + Double valueMetric, Zone zone) { + return EvaluationView.Builder + .builder() + .withRowId(rowId) + .withKeyResultId(keyResultId) + .withObjectiveId(objectiveId) + .withTeamId(1L) // Not needed because filtering of the data given is done at this point + .withQuarterId(1L) // Not needed because filtering of the data given is done at this point + .withObjectiveState(objectiveSate) + .withKeyResultType(keyResultType) + .withBaseline(baseline) + .withCommitValue(commitValue) + .withTargetValue(targetValue) + .withStretchGoal(stretchGoal) + .withValueMetric(valueMetric) + .withZone(zone) + .withLatestCheckInDate(Instant.now()) + .build(); + } + + private static final List evaluationViewList = List + .of(createEvaluationView(1L, 1L, 1L, State.ONGOING, "metric", 0D, 3D, 7D, 10D, 1D, null), + createEvaluationView(2L, 2L, 1L, State.ONGOING, "ordinal", null, null, null, null, null, Zone.TARGET), + createEvaluationView(3L, 3L, 1L, State.NOTSUCCESSFUL, "metric", 0D, 3D, 7D, 10D, 3D, null), + createEvaluationView(4L, 4L, 2L, State.SUCCESSFUL, "metric", 0D, 3D, 7D, 10D, 7D, null), + createEvaluationView(5L, 5L, 2L, State.SUCCESSFUL, "metric", 0D, 3D, 7D, 10D, 10D, null), + createEvaluationView(6L, 6L, 3L, State.ONGOING, "ordinal", null, null, null, null, null, Zone.FAIL), + createEvaluationView(7L, 7L, 3L, State.ONGOING, "ordinal", null, null, null, null, null, Zone.COMMIT), + createEvaluationView(8L, 8L, 3L, State.ONGOING, "ordinal", null, null, null, null, null, Zone.STRETCH), + createEvaluationView(9L, 9L, 3L, State.ONGOING, null, null, null, null, null, null, null), + createEvaluationView(10L, 10L, 4L, State.ONGOING, "metric", null, null, null, null, 10D, null)); } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/EvaluationViewPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/EvaluationViewPersistenceServiceIT.java index 7c6d42dcb9..bc9eef828b 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/EvaluationViewPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/EvaluationViewPersistenceServiceIT.java @@ -1,26 +1,25 @@ package ch.puzzle.okr.service.persistence; -import static ch.puzzle.okr.test.EvaluationViewTestHelper.createEvaluationView; import static org.assertj.core.api.Assertions.assertThat; import ch.puzzle.okr.models.evaluation.EvaluationView; -import ch.puzzle.okr.models.evaluation.EvaluationViewId; import ch.puzzle.okr.multitenancy.TenantContext; import ch.puzzle.okr.test.SpringIntegrationTest; import ch.puzzle.okr.test.TestHelper; +import ch.puzzle.okr.util.TeamQuarterFilter; import java.util.List; -import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @SpringIntegrationTest class EvaluationViewPersistenceServiceIT { + @Autowired private EvaluationViewPersistenceService evaluationViewPersistenceService; + @BeforeEach void setUp() { TenantContext.setCurrentTenant(TestHelper.SCHEMA_PITC); @@ -31,34 +30,24 @@ void tearDown() { TenantContext.setCurrentTenant(null); } - private static Stream evaluationViewData() { - EvaluationViewId evalViewVId_t5_q2 = new EvaluationViewId(5L, 2L); - List evalViewData_t5_q2 = List.of(2, 0, 0, 6, 2, 4, 2, 0, 4, 2, 0); - EvaluationView evalView_t5_q2 = createEvaluationView(evalViewVId_t5_q2, evalViewData_t5_q2); + @DisplayName("Should correctly return evaluation views according to params") + @Test + void shouldReturnEvaluationViews() { + List teamIds = List.of(5L); // Puzzle ITC + Long quarterId = 2L; - EvaluationViewId evalViewVId_t6_q2 = new EvaluationViewId(6L, 2L); - List evalViewData_t6_q2 = List.of(3, 0, 0, 5, 0, 5, 1, 1, 3, 0, 1); - EvaluationView evalView_t6_q2 = createEvaluationView(evalViewVId_t6_q2, evalViewData_t6_q2); + TeamQuarterFilter filter = new TeamQuarterFilter(teamIds, quarterId); - EvaluationViewId evalViewVId_t4_q2 = new EvaluationViewId(4L, 2L); - List evalViewData_t4_q2 = List.of(2, 0, 0, 5, 0, 5, 3, 1, 1, 1, 2); - EvaluationView evalView_t4_q2 = createEvaluationView(evalViewVId_t4_q2, evalViewData_t4_q2); + List evaluationViews = evaluationViewPersistenceService.findByIds(filter); - return Stream - .of(Arguments.of(List.of(evalViewVId_t5_q2), List.of(evalView_t5_q2)), - Arguments.of(List.of(evalViewVId_t6_q2), List.of(evalView_t6_q2)), - Arguments.of(List.of(evalViewVId_t4_q2), List.of(evalView_t4_q2)), - Arguments - .of(List.of(evalViewVId_t4_q2, evalViewVId_t5_q2), List.of(evalView_t4_q2, evalView_t5_q2)), - Arguments - .of(List.of(evalViewVId_t4_q2, evalViewVId_t5_q2, evalViewVId_t6_q2), - List.of(evalView_t4_q2, evalView_t5_q2, evalView_t6_q2))); - } + List expectedObjectiveIds = List.of(3L, 4L); + + List actualObjectiveIds = evaluationViews + .stream() + .map(EvaluationView::getObjectiveId) + .distinct() + .toList(); - @ParameterizedTest(name = "Should return evaluation views") - @MethodSource("evaluationViewData") - void shouldReturnEvaluationViews(List ids, List expectedEvaluationViews) { - List result = evaluationViewPersistenceService.findByIds(ids); - assertThat(result).hasSameElementsAs(expectedEvaluationViews); + assertThat(actualObjectiveIds).containsExactlyInAnyOrderElementsOf(expectedObjectiveIds); } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/validation/EvaluationViewValidationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/validation/EvaluationViewValidationServiceTest.java index a782d63570..dc0e34bb56 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/validation/EvaluationViewValidationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/validation/EvaluationViewValidationServiceTest.java @@ -2,7 +2,7 @@ import static org.mockito.Mockito.*; -import ch.puzzle.okr.models.evaluation.EvaluationViewId; +import ch.puzzle.okr.util.TeamQuarterFilter; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -24,7 +24,7 @@ class EvaluationViewValidationServiceTest { @DisplayName("Should call proper methods to validate on get") @Test void shouldCallProperMethodsToValidateOnGet() { - List ids = List.of(new EvaluationViewId(1L, 1L), new EvaluationViewId(2L, 1L)); + TeamQuarterFilter ids = new TeamQuarterFilter(List.of(1L, 2L), 1L); evaluationViewValidationService.validateOnGet(ids); verify(teamValidationService, times(2)).validateOnGet(any()); verify(quarterValidationService, times(1)).validateOnGet(any()); diff --git a/backend/src/test/java/ch/puzzle/okr/test/EvaluationViewTestHelper.java b/backend/src/test/java/ch/puzzle/okr/test/EvaluationViewTestHelper.java index ab27e5b7fa..099342c090 100644 --- a/backend/src/test/java/ch/puzzle/okr/test/EvaluationViewTestHelper.java +++ b/backend/src/test/java/ch/puzzle/okr/test/EvaluationViewTestHelper.java @@ -2,44 +2,30 @@ import ch.puzzle.okr.dto.EvaluationDto; import ch.puzzle.okr.models.evaluation.EvaluationView; -import ch.puzzle.okr.models.evaluation.EvaluationViewId; +import ch.puzzle.okr.util.TeamQuarterFilter; import java.util.List; import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; public class EvaluationViewTestHelper { private EvaluationViewTestHelper() { } - public static List getEvaluationViewIds(List teamIds, Long quarterId) { - return teamIds.stream().map(teamId -> new EvaluationViewId(teamId, quarterId)).toList(); - } + private static final AtomicLong rowIdCounter = new AtomicLong(1); - public static List generateEvaluationViews(List evaluationViewIds) { - return evaluationViewIds.stream().map(EvaluationViewTestHelper::generateRandomEvaluationView).toList(); + public static List generateEvaluationViews(TeamQuarterFilter filter) { + return filter.teamIds().stream().map(teamId -> createEvaluationView(filter.quarterId(), teamId)).toList(); } - public static EvaluationView createEvaluationView(EvaluationViewId evaluationViewId, List data) { + public static EvaluationView createEvaluationView(Long quarterId, Long teamId) { return EvaluationView.Builder .builder() - .withEvaluationViewId(evaluationViewId) - .withObjectiveAmount(data.get(0)) - .withCompletedObjectivesAmount(data.get(1)) - .withSuccessfullyCompletedObjectivesAmount(data.get(2)) - .withKeyResultAmount(data.get(3)) - .withKeyResultsOrdinalAmount(data.get(4)) - .withKeyResultsMetricAmount(data.get(5)) - .withKeyResultsInTargetOrStretchAmount(data.get(6)) - .withKeyResultsInFailAmount(data.get(7)) - .withKeyResultsInCommitAmount(data.get(8)) - .withKeyResultsInTargetAmount(data.get(9)) - .withKeyResultsInStretchAmount(data.get(10)) + .withRowId(rowIdCounter.getAndIncrement()) + .withTeamId(teamId) + .withQuarterId(quarterId) .build(); } - public static EvaluationView generateRandomEvaluationView(EvaluationViewId evaluationViewId) { - return createEvaluationView(evaluationViewId, generateEvaluationViewData()); - } - public static EvaluationDto generateEvaluationDto() { return generateEvaluationDto(generateEvaluationViewData()); } @@ -61,8 +47,4 @@ public static EvaluationDto generateEvaluationDto(List data) { public static List generateEvaluationViewData() { return new Random().ints(11, 0, 101).boxed().toList(); } - - private static int randomInt() { - return (int) (Math.random() * 101); - } }