From c452b4c454c7f8a1758492ac72e1f2569750fc1f Mon Sep 17 00:00:00 2001
From: Jonathan Foucher <jfoucher@gmail.com>
Date: Tue, 9 Aug 2022 11:32:23 +0200
Subject: [PATCH] Add maturity graph data + tests for fundamental level
 calculator

---
 .../MaturityAnswerLevelCalculator.php         |   6 +-
 .../Controllers/Api/GraphDataController.php   |  10 ++
 app/Models/EvaluationMaturityAnswer.php       |   4 +-
 app/Models/Measure.php                        |   1 +
 app/Repository/EvaluationRepository.php       | 108 +++++++++---------
 app/Repository/GraphDataRepository.php        |  17 ++-
 .../EvaluationMaturityAnswerFactory.php       |   3 +-
 database/seeders/OrganizationSeeder.php       |  19 ++-
 routes/api.php                                |   1 +
 tests/Feature/GraphDataControllerTest.php     |  60 ++++++++++
 .../MaturityAnswerLevelCalculatorTest.php     |  96 ++++++++++++++++
 11 files changed, 262 insertions(+), 63 deletions(-)
 create mode 100644 tests/Unit/MaturityAnswerLevelCalculatorTest.php

diff --git a/app/Calculator/MaturityAnswerLevelCalculator.php b/app/Calculator/MaturityAnswerLevelCalculator.php
index aa0657e2..177b2787 100644
--- a/app/Calculator/MaturityAnswerLevelCalculator.php
+++ b/app/Calculator/MaturityAnswerLevelCalculator.php
@@ -9,9 +9,11 @@ class MaturityAnswerLevelCalculator
 {
     public function calculateFundamentalLevel(Evaluation $evaluation): int
     {
-        $level = $evaluation->measureLevels->filter(function (MeasureLevel $ml) {
+        $fundamentalMeasures = $evaluation->measureLevels->filter(function (MeasureLevel $ml) {
             return true === $ml->measure->fundamental;
-        })->average(function (MeasureLevel $ml) {
+        });
+
+        $level = $fundamentalMeasures->average(function (MeasureLevel $ml) {
             return $ml->actual_level;
         });
 
diff --git a/app/Http/Controllers/Api/GraphDataController.php b/app/Http/Controllers/Api/GraphDataController.php
index e488dc23..b2a856be 100644
--- a/app/Http/Controllers/Api/GraphDataController.php
+++ b/app/Http/Controllers/Api/GraphDataController.php
@@ -45,6 +45,16 @@ public function risks(int $id): Collection|array|\Illuminate\Support\Collection
         return $this->repository->getRisksDataForEvaluation($evaluation);
     }
 
+    /**
+     * @param int $id Evaluation id
+     */
+    public function maturity(int $id): Collection|array|\Illuminate\Support\Collection
+    {
+        $evaluation = $this->getEvaluation($id);
+
+        return $this->repository->getMaturityDataForEvaluation($evaluation);
+    }
+
     /**
      * @param int $id Evaluation id
      */
diff --git a/app/Models/EvaluationMaturityAnswer.php b/app/Models/EvaluationMaturityAnswer.php
index 6f10d976..3681ad62 100644
--- a/app/Models/EvaluationMaturityAnswer.php
+++ b/app/Models/EvaluationMaturityAnswer.php
@@ -33,11 +33,11 @@ public function evaluation(): BelongsTo
 
     public function question(): BelongsTo
     {
-        return $this->belongsTo(MaturityQuestion::class);
+        return $this->belongsTo(MaturityQuestion::class, 'maturity_question_id');
     }
 
     public function answer(): BelongsTo
     {
-        return $this->belongsTo(MaturityAnswer::class);
+        return $this->belongsTo(MaturityAnswer::class, 'maturity_answer_id');
     }
 }
diff --git a/app/Models/Measure.php b/app/Models/Measure.php
index db55df09..5d61f7f7 100644
--- a/app/Models/Measure.php
+++ b/app/Models/Measure.php
@@ -66,6 +66,7 @@ class Measure extends Model
         'level3_cost' => 'string',
         'level3_duration' => 'string',
         'level3_assistance' => 'int',
+        'fundamental' => 'bool',
     ];
 
     public function scenarios(): BelongsToMany
diff --git a/app/Repository/EvaluationRepository.php b/app/Repository/EvaluationRepository.php
index f915af4f..693f6bb7 100644
--- a/app/Repository/EvaluationRepository.php
+++ b/app/Repository/EvaluationRepository.php
@@ -19,67 +19,73 @@ public function __construct(MaturityAnswerLevelCalculator $calculator)
         $this->calculator = $calculator;
     }
 
-    /**
-     * Save maturity answers
-     * @throws MissingMaturityAnswerException
-     */
-    public function saveMaturity(Evaluation $evaluation, Collection $answers, int $draft = 0)
+    public function saveAutomaticAnswers($evaluation)
     {
-        $questions = MaturityQuestion::all();
+        $questions = MaturityQuestion::where('auto', true)->get();
         foreach ($questions as $question) {
-            if ($question->auto) {
-                // Calculate answer based on data
-                // For "execution"
-                // Get completed evaluation before this one
-                // If there are none then all calculated levels are 0
-                // (nombre de mesure basculée en mesure en place dans l'évaluation en cours/nombre de mesure prévue dans évaluation précédente) *100
-                // 0 => level 0
-                // 0 < calculated_level < 20 => level 1
-                // 20 < calculated_level < 80 => level 2
-                // calculated_level >= 80 => level 3
-                // For "fondamentaux"
-                // MOYENNE.INF( niveau de la mesure en place sauvegarde, niveau de la mesure en place mot de passe, niveau de la mesure en place mise à jour , niveau de la mesure en place sensibilisation)
+            // Calculate answer based on data
+            // For "execution"
+            // Get completed evaluation before this one
+            // If there are none then all calculated levels are 0
+            // (nombre de mesure basculée en mesure en place dans l'évaluation en cours/nombre de mesure prévue dans évaluation précédente) *100
+            // 0 => level 0
+            // 0 < calculated_level < 20 => level 1
+            // 20 < calculated_level < 80 => level 2
+            // calculated_level >= 80 => level 3
+            // For "fondamentaux"
+            // MOYENNE.INF( niveau de la mesure en place sauvegarde, niveau de la mesure en place mot de passe, niveau de la mesure en place mise à jour , niveau de la mesure en place sensibilisation)
 
-                $calculated_level = 0;
-                if ('Fondamentaux' === $question->title) {
-                    $calculated_level = $this->calculator->calculateFundamentalLevel($evaluation);
-                }
+            $calculated_level = 0;
+            if ('Fondamentaux' === $question->title) {
+                $calculated_level = $this->calculator->calculateFundamentalLevel($evaluation);
+            }
 
-                if ('Exécution' === $question->title) {
-                    $calculated_level = $this->calculator->calculateExecutionLevel($evaluation);
-                }
+            if ('Exécution' === $question->title) {
+                $calculated_level = $this->calculator->calculateExecutionLevel($evaluation);
+            }
 
-                $answer = MaturityAnswer::where('maturity_question_id', $question->id)
-                    ->where('level', $calculated_level)
-                    ->first();
+            $answer = MaturityAnswer::where('maturity_question_id', $question->id)
+                ->where('level', $calculated_level)
+                ->first();
 
-                $evalAnswer = EvaluationMaturityAnswer::where('evaluation_id', $evaluation->id)
-                    ->where('maturity_question_id', $question->id)
-                    ->first();
-                if (!$evalAnswer) {
-                    $evalAnswer = new EvaluationMaturityAnswer();
-                }
+            $evalAnswer = EvaluationMaturityAnswer::where('evaluation_id', $evaluation->id)
+                ->where('maturity_question_id', $question->id)
+                ->first();
+            if (!$evalAnswer) {
+                $evalAnswer = new EvaluationMaturityAnswer();
+            }
 
-                $evalAnswer->maturity_question_id = $question->id;
-                $evalAnswer->maturity_answer_id = $answer->id;
-                $evalAnswer->evaluation_id = $evaluation->id;
-                $evalAnswer->save();
-            } else {
-                // Get answer from sent values
-                $answer = $answers->filter(function ($a) use ($question) {
-                    return $a['maturity_question_id'] === $question->id;
-                })->first();
-                if ((!isset($answer) || !isset($answer['maturity_answer_id'])) && 0 === $draft) {
-                    throw new MissingMaturityAnswerException();
-                }
+            $evalAnswer->maturity_question_id = $question->id;
+            $evalAnswer->maturity_answer_id = $answer->id;
+            $evalAnswer->evaluation_id = $evaluation->id;
+            $evalAnswer->save();
+        }
+    }
 
-                $maturity = isset($answer['id']) ? EvaluationMaturityAnswer::find($answer['id']) : new EvaluationMaturityAnswer();
-                $maturity->maturity_question_id = $answer['maturity_question_id'];
-                $maturity->maturity_answer_id = $answer['maturity_answer_id'];
-                $maturity->evaluation_id = $evaluation->id;
-                $maturity->save();
+    /**
+     * Save maturity answers
+     * @throws MissingMaturityAnswerException
+     */
+    public function saveMaturity(Evaluation $evaluation, Collection $answers, int $draft = 0)
+    {
+        $questions = MaturityQuestion::where('auto', false)->get();
+        foreach ($questions as $question) {
+            // Get answer from sent values
+            $answer = $answers->filter(function ($a) use ($question) {
+                return $a['maturity_question_id'] === $question->id;
+            })->first();
+            if ((!isset($answer) || !isset($answer['maturity_answer_id'])) && 0 === $draft) {
+                throw new MissingMaturityAnswerException();
             }
+
+            $maturity = isset($answer['id']) ? EvaluationMaturityAnswer::find($answer['id']) : new EvaluationMaturityAnswer();
+            $maturity->maturity_question_id = $answer['maturity_question_id'];
+            $maturity->maturity_answer_id = $answer['maturity_answer_id'];
+            $maturity->evaluation_id = $evaluation->id;
+            $maturity->save();
         }
+
+        $this->saveAutomaticAnswers($evaluation);
     }
 
 }
diff --git a/app/Repository/GraphDataRepository.php b/app/Repository/GraphDataRepository.php
index 63826056..efd0f599 100644
--- a/app/Repository/GraphDataRepository.php
+++ b/app/Repository/GraphDataRepository.php
@@ -2,6 +2,7 @@
 
 namespace App\Repository;
 
+use App\Calculator\MaturityAnswerLevelCalculator;
 use App\Models\Danger;
 use App\Models\DangerLevelEvaluation;
 use App\Models\Evaluation;
@@ -12,6 +13,13 @@
 
 class GraphDataRepository
 {
+    private EvaluationRepository $repository;
+
+    public function __construct(EvaluationRepository $repository)
+    {
+        $this->repository = $repository;
+    }
+
     public function getMeasuresDataForEvaluation(Evaluation $evaluation): array
     {
         $measureLevels = collect($evaluation->measureLevels)->sortBy(function (MeasureLevel $ml) {
@@ -164,6 +172,11 @@ public function getAttackDataForEvaluation(Evaluation $evaluation): array
 
     public function getMaturityDataForEvaluation(Evaluation $evaluation): array
     {
+        // Calculate automatic answer results
+        $this->repository->saveAutomaticAnswers($evaluation);
+
+        $evaluation = Evaluation::find($evaluation->id);
+
         $maturityLevels = $evaluation->maturityLevels;
 
         $values = $maturityLevels->map(function (EvaluationMaturityAnswer $ml) {
@@ -171,8 +184,8 @@ public function getMaturityDataForEvaluation(Evaluation $evaluation): array
         });
 
         return [
-            'labels' => $scenarios->pluck('name'),
-            'data' => $values,
+            'labels' => $values->pluck('label'),
+            'data' => $values->pluck('level'),
         ];
     }
 
diff --git a/database/factories/EvaluationMaturityAnswerFactory.php b/database/factories/EvaluationMaturityAnswerFactory.php
index eb2bea34..c41ce709 100644
--- a/database/factories/EvaluationMaturityAnswerFactory.php
+++ b/database/factories/EvaluationMaturityAnswerFactory.php
@@ -19,9 +19,10 @@ class EvaluationMaturityAnswerFactory extends Factory
      */
     public function definition()
     {
+        $mqid = MaturityQuestion::inRandomOrder()->first()->id;
         return [
             'maturity_question_id' => MaturityQuestion::inRandomOrder()->first()->id,
-            'maturity_answer_id' => MaturityAnswer::inRandomOrder()->first()->id,
+            'maturity_answer_id' => MaturityAnswer::where('maturity_question_id', $mqid)->inRandomOrder()->first()->id,
             'evaluation_id' => Evaluation::inRandomOrder()->first()->id,
         ];
     }
diff --git a/database/seeders/OrganizationSeeder.php b/database/seeders/OrganizationSeeder.php
index 592d181a..1289203a 100644
--- a/database/seeders/OrganizationSeeder.php
+++ b/database/seeders/OrganizationSeeder.php
@@ -32,11 +32,16 @@ public function run()
             $org->evaluations()->saveMany(Evaluation::factory(2)->make(['current_step' => 4]));
             $org->evaluations()->saveMany(Evaluation::factory(2)->make(['current_step' => 5]));
             $org->evaluations()->saveMany(Evaluation::factory(2)->make([
-                'current_step' => 5,
+                'current_step' => Evaluation::STEP_MATURITY,
                 'reference' => env('REFERENTIEL_VERSION'),
             ]));
             $org->evaluations()->saveMany(Evaluation::factory(2)->make([
-                'current_step' => 2,
+                'current_step' => Evaluation::STEP_RESULTS,
+                'status' => Evaluation::STATUS_DONE,
+                'reference' => env('REFERENTIEL_VERSION'),
+            ]));
+            $org->evaluations()->saveMany(Evaluation::factory(2)->make([
+                'current_step' => Evaluation::STEP_MEASURES,
                 'reference' => env('REFERENTIEL_VERSION'),
             ]));
             $org->evaluations()->saveMany(Evaluation::factory(2)->make(['current_step' => 1]));
@@ -66,8 +71,10 @@ public function run()
 
                 if ($eval->current_step >= Evaluation::STEP_MEASURES) {
                     $measures = Measure::all();
-                    $evalMeasures = $measures->map(function (Measure $m) {
-                        return MeasureLevel::factory()->make(['measure_id' => $m->id]);
+                    $evalMeasures = $measures->map(function (Measure $m) use ($eval) {
+                        return MeasureLevel::factory()->make([
+                            'measure_id' => $m->id,
+                        ]);
                     });
                     $eval->measureLevels()->saveMany($evalMeasures);
                 }
@@ -77,7 +84,9 @@ public function run()
                     $answers = $questions->map(function (MaturityQuestion $m) {
                         $a = new EvaluationMaturityAnswer();
                         $a->maturity_question_id = $m->id;
-                        $a->maturity_answer_id = MaturityAnswer::inRandomOrder()->first()->id;
+                        $a->maturity_answer_id = MaturityAnswer::where('maturity_question_id', $m->id)
+                            ->inRandomOrder()
+                            ->first()->id;
 
                         return $a;
                     });
diff --git a/routes/api.php b/routes/api.php
index d9248f48..7e9b2df9 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -57,6 +57,7 @@
     Route::get('/{id}/graphs/futurerisks', [GraphDataController::class, 'futurerisks'])->name('api.evaluations.graph.futurerisks');
     Route::get('/{id}/graphs/exposition', [GraphDataController::class, 'exposition'])->name('api.evaluations.graph.exposition');
     Route::get('/{id}/graphs/attack', [GraphDataController::class, 'attack'])->name('api.evaluations.graph.attack');
+    Route::get('/{id}/graphs/maturity', [GraphDataController::class, 'maturity'])->name('api.evaluations.graph.maturity');
     Route::delete('/{id}', [EvaluationsController::class, 'delete'])->name('api.evaluations.delete');
 });
 Route::prefix('/dangers')->middleware('auth:sanctum')->group(function () {
diff --git a/tests/Feature/GraphDataControllerTest.php b/tests/Feature/GraphDataControllerTest.php
index 0a4b1666..663d50d5 100644
--- a/tests/Feature/GraphDataControllerTest.php
+++ b/tests/Feature/GraphDataControllerTest.php
@@ -4,7 +4,10 @@
 
 use App\Models\DangerLevel;
 use App\Models\Evaluation;
+use App\Models\EvaluationMaturityAnswer;
+use App\Models\MaturityAnswer;
 use App\Models\User;
+use App\Repository\EvaluationRepository;
 use Illuminate\Foundation\Testing\RefreshDatabase;
 use Tests\TestCase;
 
@@ -207,4 +210,61 @@ public function testCheckEvaluationExpositionGraphData()
 
         $this->assertEquals($expected, $data);
     }
+
+
+    public function testCheckEvaluationMaturityGraphData()
+    {
+        /**
+         * @var Evaluation $evaluation
+         */
+        $evaluation = Evaluation::with('dangerLevels')
+            ->with('maturityLevels')
+            ->with('measureLevels')
+            ->where('current_step', '>=', 5)
+            ->where('status', Evaluation::STATUS_DONE)
+            ->first();
+
+        // Set all levels to know quantity
+        foreach ($evaluation->measureLevels as $k => $measureLevel) {
+            $measureLevel->actual_level = 3;
+            $measureLevel->save();
+        }
+
+        foreach ($evaluation->maturityLevels as $k => $maturityLevel) {
+            $maturityLevel->maturity_answer_id = MaturityAnswer::where('level', $k % 3)
+                ->where('maturity_question_id', $maturityLevel->maturity_question_id)
+                ->first()->id;
+
+            $maturityLevel->save();
+        }
+
+        $admin = User::where('role', User::ROLE_ADMIN)->first();
+
+        $response = $this->actingAs($admin)->getJson(route('api.evaluations.graph.maturity', ['id' => $evaluation->id]));
+
+        $response->assertOk();
+
+        $data = $response->json();
+
+        $expected = array (
+            'labels' =>
+                array (
+                    0 => 'Connaissance',
+                    1 => 'Organisation',
+                    2 => 'Motivation',
+                    3 => 'Exécution',
+                    4 => 'Fondamentaux',
+                ),
+            'data' =>
+                array (
+                    0 => 0,
+                    1 => 1,
+                    2 => 2,
+                    3 => 0,
+                    4 => 3,
+                ),
+        );
+
+        $this->assertEquals($expected, $data);
+    }
 }
diff --git a/tests/Unit/MaturityAnswerLevelCalculatorTest.php b/tests/Unit/MaturityAnswerLevelCalculatorTest.php
new file mode 100644
index 00000000..91dda54c
--- /dev/null
+++ b/tests/Unit/MaturityAnswerLevelCalculatorTest.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Tests\Unit;
+
+use App\Calculator\MaturityAnswerLevelCalculator;
+use App\Models\Evaluation;
+use App\Models\Measure;
+use App\Models\MeasureLevel;
+use Tests\TestCase;
+
+class MaturityAnswerLevelCalculatorTest extends TestCase
+{
+    public function testCalculateFundamentalLevelWithOneLevel()
+    {
+        $evaluation = new Evaluation();
+
+        $measure = new Measure();
+        $measure->fundamental = true;
+
+        $measureLevel = new MeasureLevel();
+        $measureLevel->measure = $measure;
+        $measureLevel->evaluation = $evaluation;
+        $measureLevel->actual_level = 3;
+
+        $evaluation->measureLevels->add($measureLevel);
+
+        $calculator = new MaturityAnswerLevelCalculator();
+
+        $res = $calculator->calculateFundamentalLevel($evaluation);
+
+        $this->assertEquals(3, $res);
+    }
+
+    public function testCalculateFundamentalLevelWithSeveralLevels()
+    {
+        $evaluation = new Evaluation();
+
+        $measure = new Measure();
+        $measure->fundamental = true;
+
+        $measureLevel = new MeasureLevel();
+        $measureLevel->measure = $measure;
+        $measureLevel->evaluation = $evaluation;
+        $measureLevel->actual_level = 3;
+
+        $evaluation->measureLevels->add($measureLevel);
+
+        $measure = new Measure();
+        $measure->fundamental = true;
+
+        $measureLevel = new MeasureLevel();
+        $measureLevel->measure = $measure;
+        $measureLevel->evaluation = $evaluation;
+        $measureLevel->actual_level = 1;
+
+        $evaluation->measureLevels->add($measureLevel);
+
+        $calculator = new MaturityAnswerLevelCalculator();
+
+        $res = $calculator->calculateFundamentalLevel($evaluation);
+
+        $this->assertEquals(2, $res);
+    }
+
+
+    public function testCalculateFundamentalLevelWithNonFundamentalLevels()
+    {
+        $evaluation = new Evaluation();
+
+        $measure = new Measure();
+        $measure->fundamental = true;
+
+        $measureLevel = new MeasureLevel();
+        $measureLevel->measure = $measure;
+        $measureLevel->evaluation = $evaluation;
+        $measureLevel->actual_level = 3;
+
+        $evaluation->measureLevels->add($measureLevel);
+
+        $measure = new Measure();
+        $measure->fundamental = false;
+
+        $measureLevel = new MeasureLevel();
+        $measureLevel->measure = $measure;
+        $measureLevel->evaluation = $evaluation;
+        $measureLevel->actual_level = 1;
+
+        $evaluation->measureLevels->add($measureLevel);
+
+        $calculator = new MaturityAnswerLevelCalculator();
+
+        $res = $calculator->calculateFundamentalLevel($evaluation);
+
+        $this->assertEquals(3, $res);
+    }
+}
-- 
GitLab