diff --git a/.github/scripts/check_api.sh b/.github/scripts/check_api.sh index b27f17fa..37cbdbab 100755 --- a/.github/scripts/check_api.sh +++ b/.github/scripts/check_api.sh @@ -1,5 +1,6 @@ #!/bin/sh -set -e +set -x # print commands executed +set -e # exit immediatly if any command fails # Usage : check_api.sh api_ip keycloak_api , both arguments optional defaulted to $(minikube ip) # ./check_api.sh localhost:8085 localhost:8180 @@ -266,4 +267,17 @@ req_identified_risks=$(curl -X POST "http://$api_ip/pathfinder/assessments/risks -H 'Content-Type: application/json' -s -w "%{http_code}" -o /dev/null) test "$(echo $req_identified_risks)" = "400" + +echo +echo +echo "19 >>> Checking the confidence of assessments" +confidence=$(curl -X POST "http://$api_ip/pathfinder/assessments/confidence" -H 'Accept: application/json' \ + -H "Authorization: Bearer $access_token" \ + -d "[{\"applicationId\":100} , {\"applicationId\": $applicationTarget}]" \ + -H 'Content-Type: application/json' ) +echo $confidence | grep "\"assessmentId\":$assessmentCopiedId" +echo $confidence | grep "\"confidence\":" +echo $confidence | grep "\"applicationId\":$applicationTarget" + + echo " +++++ API CHECK SUCCESSFUL ++++++" diff --git a/src/main/java/io/tackle/pathfinder/controllers/AssessmentsResource.java b/src/main/java/io/tackle/pathfinder/controllers/AssessmentsResource.java index 4a260579..3af3280e 100644 --- a/src/main/java/io/tackle/pathfinder/controllers/AssessmentsResource.java +++ b/src/main/java/io/tackle/pathfinder/controllers/AssessmentsResource.java @@ -1,5 +1,6 @@ package io.tackle.pathfinder.controllers; +import io.tackle.pathfinder.dto.AdoptionCandidateDto; import io.tackle.pathfinder.dto.ApplicationDto; import io.tackle.pathfinder.dto.AssessmentDto; import io.tackle.pathfinder.dto.AssessmentHeaderDto; @@ -93,4 +94,12 @@ public List getLandscape(@NotNull @Valid List appl return service.landscape(applicationIds.stream().map(e -> e.getApplicationId()).collect(Collectors.toList())); } + @POST + @Path("/confidence") + @Produces("application/json") + @Consumes("application/json") + public List adoptionCandidate(@NotNull @Valid List applicationId) { + return service.getAdoptionCandidate(applicationId.stream().map(a -> a.getApplicationId()).collect(Collectors.toList())); + } + } diff --git a/src/main/java/io/tackle/pathfinder/dto/AdoptionCandidateDto.java b/src/main/java/io/tackle/pathfinder/dto/AdoptionCandidateDto.java new file mode 100644 index 00000000..87dd830b --- /dev/null +++ b/src/main/java/io/tackle/pathfinder/dto/AdoptionCandidateDto.java @@ -0,0 +1,20 @@ +package io.tackle.pathfinder.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@RegisterForReflection +public class AdoptionCandidateDto { + public Long applicationId; + public Long assessmentId; + public Integer confidence; +} diff --git a/src/main/java/io/tackle/pathfinder/services/AssessmentSvc.java b/src/main/java/io/tackle/pathfinder/services/AssessmentSvc.java index 29fe8ed3..324cc008 100644 --- a/src/main/java/io/tackle/pathfinder/services/AssessmentSvc.java +++ b/src/main/java/io/tackle/pathfinder/services/AssessmentSvc.java @@ -1,5 +1,6 @@ package io.tackle.pathfinder.services; +import com.google.common.util.concurrent.AtomicDouble; import io.tackle.pathfinder.dto.*; import io.tackle.pathfinder.mapper.AssessmentMapper; import io.tackle.pathfinder.model.Risk; @@ -16,6 +17,7 @@ import io.tackle.pathfinder.model.questionnaire.SingleOption; import lombok.Value; import lombok.extern.java.Log; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.apache.commons.lang3.StringUtils; import javax.enterprise.context.ApplicationScoped; @@ -31,8 +33,11 @@ import java.util.*; import java.util.function.Function; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.logging.Level; import java.util.stream.Collectors; +import java.util.stream.Stream; import static java.util.stream.Collectors.toList; @@ -45,6 +50,29 @@ public class AssessmentSvc { @Inject EntityManager entityManager; + @ConfigProperty(name = "confidence.risk.RED.weight") + Integer redWeight; + @ConfigProperty(name = "confidence.risk.GREEN.weight") + Integer greenWeight; + @ConfigProperty(name = "confidence.risk.AMBER.weight") + Integer amberWeight; + @ConfigProperty(name = "confidence.risk.UNKNOWN.weight") + Integer unknownWeight; + + @ConfigProperty(name = "confidence.risk.AMBER.multiplier") + Double amberMultiplier; + @ConfigProperty(name = "confidence.risk.RED.multiplier") + Double redMultiplier; + + @ConfigProperty(name = "confidence.risk.RED.adjuster") + Double redAdjuster; + @ConfigProperty(name = "confidence.risk.AMBER.adjuster") + Double amberAdjuster; + @ConfigProperty(name = "confidence.risk.GREEN.adjuster") + Double greenAdjuster; + @ConfigProperty(name = "confidence.risk.UNKNOWN.adjuster") + Double unknownAdjuster; + public Optional getAssessmentHeaderDtoByApplicationId(@NotNull Long applicationId) { List assessmentQuery = Assessment.list("application_id", applicationId); return assessmentQuery.stream().findFirst().map(e -> mapper.assessmentToAssessmentHeaderDto(e)); @@ -357,6 +385,49 @@ public List identifiedRisks(List applicationList) { Query query = entityManager.createNativeQuery(sqlString); return mapper.riskListQueryToRiskLineDtoList(query.getResultList()); } + @Transactional + public List getAdoptionCandidate(List applicationId) { + return applicationId.stream() + .map(a-> Assessment.find("applicationId", a).firstResultOptional()) + .filter(b -> b.isPresent()) + .map(c -> new AdoptionCandidateDto(((Assessment) c.get()).applicationId, ((Assessment) c.get()).id, calculateConfidence((Assessment) c.get()))) + .collect(Collectors.toList()); + } + + private Integer calculateConfidence(Assessment assessment) { + Map weightMap = Map.of(Risk.RED, redWeight, + Risk.UNKNOWN, unknownWeight, + Risk.AMBER, amberWeight, + Risk.GREEN, greenWeight); + + List answeredOptions = assessment.assessmentQuestionnaire.categories.stream() + .flatMap(cat -> cat.questions.stream()) + .flatMap(que -> que.singleOptions.stream()) + .filter(opt -> opt.selected) + .collect(Collectors.toList()); + long totalAnswered = answeredOptions.stream().count(); + + // Grouping to know how many answers per Risk + Map answersCountByRisk = answeredOptions.stream() + .collect(Collectors.groupingBy(a -> a.risk, Collectors.counting())); + + + BigDecimal result = getConfidenceTacklePathfinder(weightMap, answeredOptions, totalAnswered, answersCountByRisk); + + return result.intValue(); + } + + private BigDecimal getConfidenceTacklePathfinder(Map weightMap, List answeredOptions, long totalAnswered, Map answersCountByRisk) { + Map adjusterBase = Map.of(Risk.RED, redAdjuster, Risk.AMBER, amberAdjuster, Risk.GREEN, greenAdjuster, Risk.UNKNOWN, unknownAdjuster); + + double answeredWeight = answeredOptions.stream().mapToDouble(a -> weightMap.get(a.risk) * adjusterBase.getOrDefault(a.risk, 1d)).sum(); + + long maxWeight = weightMap.get(Risk.GREEN) * totalAnswered; + + BigDecimal result = new BigDecimal(answeredWeight / maxWeight * 100); + result.setScale(0, RoundingMode.DOWN); + return result; + } private AssessmentRiskDto sqlRowToAssessmentRisk(Object row) { return new AssessmentRiskDto((Integer) ((Object[]) row)[0], (String) ((Object[]) row)[1], (Integer) ((Object[]) row)[2]); diff --git a/src/main/resources/META-INF/openapi.json b/src/main/resources/META-INF/openapi.json index 818c5bd0..9bbec08d 100644 --- a/src/main/resources/META-INF/openapi.json +++ b/src/main/resources/META-INF/openapi.json @@ -563,313 +563,6 @@ } ] }, - "/assessments/risks": { - "summary": "Will retrieve the risks for filtered assessments", - "description": "The response will contain only those applications assessed. This can end on having 0 elements in the response.", - "get": { - "parameters": [ - { - "name": "applications", - "description": "", - "schema": { - "type": "array", - "items": { - "type": "integer" - } - }, - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RiskLine" - } - }, - "examples": { - "risks_ok": { - "value": [ - { - "category": "some text", - "question": "some text", - "answer": "some text", - "applications": [ - 89, - 32 - ] - }, - { - "category": "some text", - "question": "some text", - "answer": "some text", - "applications": [ - 74, - 5 - ] - } - ] - } - } - } - }, - "description": "On successfull response" - } - } - } - }, - "/assessments/confidence": { - "post": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Application" - } - }, - "examples": { - "post_confidence_request": { - "value": [ - { - "applicationId": 5 - }, - { - "applicationId": 51 - } - ] - } - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AssessmentConfidence" - } - }, - "examples": { - "confidence_response_ok": { - "value": [ - { - "assessmentId": 48, - "confidence": 83 - }, - { - "assessmentId": 80, - "confidence": 10 - } - ] - } - } - } - }, - "description": "On success" - } - } - } - }, - "/assessments/assessment-risk": { - "post": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Application" - } - }, - "examples": { - "assessment-risk-ok": { - "value": [ - { - "applicationId": 5 - }, - { - "applicationId": 37 - } - ] - } - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AssessmentRisk" - } - }, - "examples": { - "assessment-risk-ok-resp": { - "value": [ - { - "assessmentId": 34, - "risk": "RED" - }, - { - "assessmentId": 69, - "risk": "UNKNOWN" - } - ] - } - } - } - }, - "description": "Success response" - } - } - } - }, - "/assessments/bulk": { - "post": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "integer" - } - }, - "examples": { - "bulk_request": { - "value": [ - 74, - 54 - ] - } - } - } - }, - "required": true - }, - "parameters": [ - { - "name": "fromAssessmentId", - "description": "", - "schema": { - "type": "integer" - }, - "in": "query", - "required": false - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssessmentBulk" - }, - "examples": { - "assessment_bulk_post": { - "value": { - "bulkId": 58, - "applications": [ - 9, - 45 - ], - "fromAssessmentId": 74, - "createdAssessments": [ - { - "status": "COMPLETE", - "applicationId": 57, - "id": 50, - "error": "some text" - }, - { - "status": "STARTED", - "applicationId": 6, - "id": 13, - "error": "some text" - } - ] - } - } - } - } - }, - "description": "When the operation finishes successfully." - } - }, - "operationId": "bulkCreateAssessment", - "summary": "Async call to create a list of assessments for the given applications.", - "description": "It will allow to copy the assessment answers from a given assessment ( in query params )" - } - }, - "/assessments/bulk/{bulkId}": { - "get": { - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssessmentBulk" - }, - "examples": { - "bulk_response_ok": { - "value": { - "bulkId": 31, - "applications": [ - 92, - 0 - ], - "fromAssessmentId": 72, - "createdAssessments": [ - { - "status": "STARTED", - "applicationId": 90, - "id": 52, - "error": "some text" - }, - { - "status": "COMPLETE", - "applicationId": 28, - "id": 18, - "error": "some text" - } - ] - } - } - } - } - }, - "description": "If bulk process is found" - }, - "404": { - "description": "If bulk process is not found" - } - } - }, - "parameters": [ - { - "name": "bulkId", - "schema": { - "type": "integer" - }, - "in": "path", - "required": true - } - ] - }, "/assessments/risks": { "summary": "Will retrieve the risks for filtered assessments", "description": "The response will contain only those applications assessed. This can end on having 0 elements in the response.", @@ -1789,7 +1482,8 @@ "description": "", "required": [ "assessmentId", - "confidence" + "confidence", + "applicationId" ], "type": "object", "properties": { @@ -1800,6 +1494,10 @@ "confidence": { "description": "", "type": "integer" + }, + "applicationId": { + "description": "", + "type": "integer" } } }, diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ee1812f4..0a229646 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -39,6 +39,20 @@ quarkus.openshift.part-of=${quarkus.kubernetes.part-of} quarkus.kubernetes.labels."app.kubernetes.io/component"=rest quarkus.openshift.labels."app.kubernetes.io/component"=rest +# Service +confidence.risk.RED.weight=1 +confidence.risk.AMBER.weight=800 +confidence.risk.GREEN.weight=1000 +confidence.risk.UNKNOWN.weight=700 + +confidence.risk.RED.multiplier=0.6 +confidence.risk.AMBER.multiplier=0.95 + +confidence.risk.AMBER.adjuster=0.98 +confidence.risk.UNKNOWN.adjuster=1.0 +confidence.risk.RED.adjuster=0.5 +confidence.risk.GREEN.adjuster=1.0 + # ----- PROD %prod.quarkus.hibernate-orm.log.sql=false @@ -63,6 +77,8 @@ quarkus.openshift.labels."app.kubernetes.io/component"=rest %dev.quarkus.hibernate-orm.database.generation=none + + # -------- Extra profiles %local.quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/pathfinder_db %local.quarkus.oidc.auth-server-url=https://localhost:8543/auth/realms/quarkus diff --git a/src/test/java/io/tackle/pathfinder/controllers/AssessmentsResourceTest.java b/src/test/java/io/tackle/pathfinder/controllers/AssessmentsResourceTest.java index 8407c990..4da4b455 100644 --- a/src/test/java/io/tackle/pathfinder/controllers/AssessmentsResourceTest.java +++ b/src/test/java/io/tackle/pathfinder/controllers/AssessmentsResourceTest.java @@ -9,7 +9,6 @@ import io.tackle.commons.testcontainers.PostgreSQLDatabaseTestResource; import io.tackle.commons.tests.SecuredResourceTest; import io.tackle.pathfinder.dto.*; -import io.tackle.pathfinder.dto.*; import io.tackle.pathfinder.model.Risk; import io.tackle.pathfinder.model.assessment.Assessment; import io.tackle.pathfinder.model.assessment.AssessmentCategory; @@ -24,7 +23,7 @@ import org.junit.jupiter.api.Test; import javax.inject.Inject; -import javax.transaction.Transactional; +import javax.transaction.*; import java.time.Duration; import java.time.LocalTime; @@ -60,6 +59,9 @@ public class AssessmentsResourceTest extends SecuredResourceTest { @Inject ManagedExecutor managedExecutor; + @Inject + UserTransaction userTransaction; + @BeforeEach @Transactional public void init() { @@ -923,4 +925,45 @@ public void given_ListOfApplicationsWithNoAssessments_when_IdentifiedRisks_then_ .statusCode(200) .body("size()", is(0)); } + + @Test + public void given_ApplicationsAssessed_When_Confidence_Then_ResultIsTheExpected() throws SystemException, NotSupportedException, HeuristicRollbackException, HeuristicMixedException, RollbackException { + userTransaction.begin(); + // create assessment + AssessmentHeaderDto assessmentREDHeader = assessmentSvc.createAssessment( 20008L); + AssessmentHeaderDto assessmentGREENHeader = assessmentSvc.createAssessment( 20009L); + AssessmentHeaderDto assessmentAMBERHeader = assessmentSvc.createAssessment( 20010L); + AssessmentHeaderDto assessmentUNKNOWNHeader = assessmentSvc.createAssessment( 20011L); + Assessment assessmentRED = Assessment.findById(assessmentREDHeader.getId()); + Assessment assessmentGREEN = Assessment.findById(assessmentGREENHeader.getId()); + Assessment assessmentAMBER = Assessment.findById(assessmentAMBERHeader.getId()); + Assessment assessmentUNKNOWN = Assessment.findById(assessmentUNKNOWNHeader.getId()); + + // answer questions + assessmentRED.status = AssessmentStatus.COMPLETE; + assessmentRED.assessmentQuestionnaire.categories.forEach(e -> e.questions.forEach(f -> f.singleOptions.stream().filter(a -> a.risk == Risk.RED).findFirst().ifPresent(b -> b.selected = true))); + assessmentGREEN.status = AssessmentStatus.COMPLETE; + assessmentGREEN.assessmentQuestionnaire.categories.forEach(e -> e.questions.forEach(f -> f.singleOptions.stream().filter(a -> a.risk == Risk.GREEN).findFirst().ifPresent(b -> b.selected = true))); + assessmentAMBER.status = AssessmentStatus.COMPLETE; + assessmentAMBER.assessmentQuestionnaire.categories.forEach(e -> e.questions.forEach(f -> f.singleOptions.stream().filter(a -> a.risk == Risk.AMBER).findFirst().ifPresent(b -> b.selected = true))); + assessmentUNKNOWN.status = AssessmentStatus.COMPLETE; + assessmentUNKNOWN.assessmentQuestionnaire.categories.forEach(e -> e.questions.forEach(f -> f.singleOptions.stream().filter(a -> a.risk == Risk.UNKNOWN).findFirst().ifPresent(b -> b.selected = true))); + + userTransaction.commit(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body(List.of(new ApplicationDto(20008L),new ApplicationDto(20009L),new ApplicationDto(20010L),new ApplicationDto(20011L))) + .when() + .post("/assessments/confidence") + .then() + .log().all() + .statusCode(200) + .body("find{it.assessmentId=="+ assessmentRED.id + "}.confidence", is(0)) + .body("find{it.assessmentId=="+ assessmentGREEN.id + "}.confidence", is(100)) + .body("find{it.assessmentId=="+ assessmentAMBER.id + "}.confidence", is(78)) // vs old pathfinder formula : 25 + .body("find{it.assessmentId=="+ assessmentUNKNOWN.id + "}.confidence", is(70)); + + } } diff --git a/src/test/java/io/tackle/pathfinder/services/AssessmentSvcTest.java b/src/test/java/io/tackle/pathfinder/services/AssessmentSvcTest.java index f08bdf2b..1d01e496 100644 --- a/src/test/java/io/tackle/pathfinder/services/AssessmentSvcTest.java +++ b/src/test/java/io/tackle/pathfinder/services/AssessmentSvcTest.java @@ -456,6 +456,39 @@ public void given_AssessmentsNotCompleted_When_landscape_Then_ThoseAssessmentsAr } + @Test + @Transactional + public void given_ApplicationsAssessed_when_AdoptionCandidate_then_ResuletIsTheExpected() { + // create assessment + AssessmentHeaderDto assessmentREDHeader = assessmentSvc.createAssessment( 10008L); + AssessmentHeaderDto assessmentGREENHeader = assessmentSvc.createAssessment( 10009L); + AssessmentHeaderDto assessmentAMBERHeader = assessmentSvc.createAssessment( 10010L); + AssessmentHeaderDto assessmentUNKNOWNHeader = assessmentSvc.createAssessment( 10011L); + Assessment assessmentRED = Assessment.findById(assessmentREDHeader.getId()); + Assessment assessmentGREEN = Assessment.findById(assessmentGREENHeader.getId()); + Assessment assessmentAMBER = Assessment.findById(assessmentAMBERHeader.getId()); + Assessment assessmentUNKNOWN = Assessment.findById(assessmentUNKNOWNHeader.getId()); + + // answer questions + assessmentRED.status = AssessmentStatus.COMPLETE; + assessmentRED.assessmentQuestionnaire.categories.forEach(e -> e.questions.forEach(f -> f.singleOptions.stream().filter(a -> a.risk == Risk.RED).findFirst().ifPresent(b -> b.selected = true))); + assessmentGREEN.status = AssessmentStatus.COMPLETE; + assessmentGREEN.assessmentQuestionnaire.categories.forEach(e -> e.questions.forEach(f -> f.singleOptions.stream().filter(a -> a.risk == Risk.GREEN).findFirst().ifPresent(b -> b.selected = true))); + assessmentAMBER.status = AssessmentStatus.COMPLETE; + assessmentAMBER.assessmentQuestionnaire.categories.forEach(e -> e.questions.forEach(f -> f.singleOptions.stream().filter(a -> a.risk == Risk.AMBER).findFirst().ifPresent(b -> b.selected = true))); + assessmentUNKNOWN.status = AssessmentStatus.COMPLETE; + assessmentUNKNOWN.assessmentQuestionnaire.categories.forEach(e -> e.questions.forEach(f -> f.singleOptions.stream().filter(a -> a.risk == Risk.UNKNOWN).findFirst().ifPresent(b -> b.selected = true))); + + // get confidence + List adoptionCandidate = assessmentSvc.getAdoptionCandidate(List.of(10008L, 10009L, 10010L, 10011L, 99999955L)); + + // assert + assertThat(adoptionCandidate).containsExactlyInAnyOrder(new AdoptionCandidateDto(assessmentREDHeader.getApplicationId(), assessmentREDHeader.getId(), 0), + new AdoptionCandidateDto(assessmentGREENHeader.getApplicationId(), assessmentGREENHeader.getId(), 100), + new AdoptionCandidateDto(assessmentAMBERHeader.getApplicationId(), assessmentAMBERHeader.getId(), 78), // vs 25 in old pathfinder + new AdoptionCandidateDto(assessmentUNKNOWNHeader.getApplicationId(), assessmentUNKNOWNHeader.getId(), 70)); + } + @Transactional public Assessment createAssessment(Questionnaire questionnaire, long applicationId) { log.info("Creating an assessment ");