Files
Classeo/backend/tests/Unit/Scolarite/Application/Query/GetChildrenGrades/GetChildrenGradesSummaryHandlerTest.php
Mathias STRASSER 272d31e1c0
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
feat: Permettre à l'élève de consulter ses notes et moyennes
L'élève avait accès à ses compétences mais pas à ses notes numériques.
Cette fonctionnalité lui donne une vue complète de sa progression scolaire
avec moyennes par matière, détail par évaluation, statistiques de classe,
et un mode "découverte" pour révéler ses notes à son rythme (FR14, FR15).

Les notes ne sont visibles qu'après publication par l'enseignant, ce qui
garantit que l'élève les découvre avant ses parents (délai 24h story 6.7).
2026-04-07 10:00:28 +02:00

311 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Scolarite\Application\Query\GetChildrenGrades;
use App\Scolarite\Application\Port\ParentChildrenReader;
use App\Scolarite\Application\Port\ParentGradeDelayReader;
use App\Scolarite\Application\Port\ScheduleDisplayReader;
use App\Scolarite\Application\Query\GetChildrenGrades\GetChildrenGradesHandler;
use App\Scolarite\Application\Query\GetChildrenGrades\GetChildrenGradesSummaryHandler;
use App\Scolarite\Application\Query\GetChildrenGrades\GetChildrenGradesSummaryQuery;
use App\Scolarite\Domain\Model\Grade\Grade;
use App\Scolarite\Domain\Model\Grade\GradeStatus;
use App\Scolarite\Domain\Policy\VisibiliteNotesPolicy;
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryEvaluationRepository;
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryEvaluationStatisticsRepository;
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryGradeRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class GetChildrenGradesSummaryHandlerTest extends TestCase
{
use ParentGradeTestHelper;
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
private const string PARENT_ID = '550e8400-e29b-41d4-a716-446655440060';
private const string CHILD_A_ID = '550e8400-e29b-41d4-a716-446655440050';
private const string CHILD_B_ID = '550e8400-e29b-41d4-a716-446655440051';
private const string CLASS_A_ID = '550e8400-e29b-41d4-a716-446655440020';
private const string SUBJECT_MATH_ID = '550e8400-e29b-41d4-a716-446655440030';
private const string SUBJECT_FRENCH_ID = '550e8400-e29b-41d4-a716-446655440031';
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
private InMemoryEvaluationRepository $evaluationRepository;
private InMemoryGradeRepository $gradeRepository;
private InMemoryEvaluationStatisticsRepository $statisticsRepository;
private DateTimeImmutable $now;
protected function setUp(): void
{
$this->evaluationRepository = new InMemoryEvaluationRepository();
$this->gradeRepository = new InMemoryGradeRepository();
$this->statisticsRepository = new InMemoryEvaluationStatisticsRepository();
$this->now = new DateTimeImmutable('2026-04-06 14:00:00');
}
#[Test]
public function itReturnsEmptyWhenParentHasNoChildren(): void
{
$handler = $this->createHandler(children: []);
$result = $handler(new GetChildrenGradesSummaryQuery(
parentId: self::PARENT_ID,
tenantId: self::TENANT_ID,
));
self::assertSame([], $result);
}
#[Test]
public function itComputesAveragesFromVisibleGrades(): void
{
// Maths: 16/20 coeff 2 + 12/20 coeff 1 → weighted = (32+12)/3 = 14.67
$eval1 = $this->givenPublishedEvaluation(
title: 'DS Maths',
subjectId: self::SUBJECT_MATH_ID,
publishedAt: '2026-04-03 10:00:00',
coefficient: 2.0,
);
$this->givenGrade($eval1, self::CHILD_A_ID, 16.0);
$eval2 = $this->givenPublishedEvaluation(
title: 'Contrôle Maths',
subjectId: self::SUBJECT_MATH_ID,
publishedAt: '2026-04-03 12:00:00',
coefficient: 1.0,
);
$this->givenGrade($eval2, self::CHILD_A_ID, 12.0);
// Français: 15/20 coeff 1 → 15.0
$eval3 = $this->givenPublishedEvaluation(
title: 'Dictée',
subjectId: self::SUBJECT_FRENCH_ID,
publishedAt: '2026-04-03 14:00:00',
coefficient: 1.0,
);
$this->givenGrade($eval3, self::CHILD_A_ID, 15.0);
$handler = $this->createHandler(
children: [
['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'],
],
);
$result = $handler(new GetChildrenGradesSummaryQuery(
parentId: self::PARENT_ID,
tenantId: self::TENANT_ID,
));
self::assertCount(1, $result);
self::assertSame(self::CHILD_A_ID, $result[0]->childId);
self::assertCount(2, $result[0]->subjectAverages);
// General = mean of subject averages = (14.67 + 15.0) / 2 = 14.84
self::assertNotNull($result[0]->generalAverage);
self::assertEqualsWithDelta(14.84, $result[0]->generalAverage, 0.01);
}
#[Test]
public function itRespectsDelayAndExcludesRecentGradesFromAverages(): void
{
// Visible grade (48h ago)
$evalOld = $this->givenPublishedEvaluation(
title: 'Ancien',
subjectId: self::SUBJECT_MATH_ID,
publishedAt: '2026-04-04 10:00:00',
);
$this->givenGrade($evalOld, self::CHILD_A_ID, 10.0);
// Not yet visible (12h ago, within 24h delay)
$evalRecent = $this->givenPublishedEvaluation(
title: 'Récent',
subjectId: self::SUBJECT_MATH_ID,
publishedAt: '2026-04-06 02:00:00',
);
$this->givenGrade($evalRecent, self::CHILD_A_ID, 20.0);
$handler = $this->createHandler(
children: [
['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'],
],
);
$result = $handler(new GetChildrenGradesSummaryQuery(
parentId: self::PARENT_ID,
tenantId: self::TENANT_ID,
));
self::assertCount(1, $result);
// Only the old grade (10.0) should be in the average, not the recent one (20.0)
self::assertSame(10.0, $result[0]->generalAverage);
}
#[Test]
public function itReturnsNullAverageWhenNoVisibleGrades(): void
{
$handler = $this->createHandler(
children: [
['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'],
],
);
$result = $handler(new GetChildrenGradesSummaryQuery(
parentId: self::PARENT_ID,
tenantId: self::TENANT_ID,
));
self::assertCount(1, $result);
self::assertSame([], $result[0]->subjectAverages);
self::assertNull($result[0]->generalAverage);
}
#[Test]
public function itReturnsNullAverageWhenAllGradesHaveNullValue(): void
{
// Grades with null value (status ABSENT / DISPENSED) should not contribute to averages
$eval1 = $this->givenPublishedEvaluation(
title: 'DS Maths',
subjectId: self::SUBJECT_MATH_ID,
publishedAt: '2026-04-03 10:00:00',
);
$this->givenGradeWithStatus($eval1, self::CHILD_A_ID, GradeStatus::ABSENT);
$eval2 = $this->givenPublishedEvaluation(
title: 'Dictee',
subjectId: self::SUBJECT_FRENCH_ID,
publishedAt: '2026-04-03 12:00:00',
);
$this->givenGradeWithStatus($eval2, self::CHILD_A_ID, GradeStatus::DISPENSED);
$handler = $this->createHandler(
children: [
['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'],
],
);
$result = $handler(new GetChildrenGradesSummaryQuery(
parentId: self::PARENT_ID,
tenantId: self::TENANT_ID,
));
self::assertCount(1, $result);
self::assertSame([], $result[0]->subjectAverages);
self::assertNull($result[0]->generalAverage);
}
#[Test]
public function itReturnsAveragesForMultipleChildren(): void
{
$evalA = $this->givenPublishedEvaluation(
title: 'Maths A',
publishedAt: '2026-04-03 10:00:00',
);
$this->givenGrade($evalA, self::CHILD_A_ID, 16.0);
$evalB = $this->givenPublishedEvaluation(
title: 'Maths B',
classId: '550e8400-e29b-41d4-a716-446655440021',
publishedAt: '2026-04-03 10:00:00',
);
$this->givenGrade($evalB, self::CHILD_B_ID, 8.0);
$handler = $this->createHandler(
children: [
['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'],
['studentId' => self::CHILD_B_ID, 'firstName' => 'Lucas', 'lastName' => 'Dupont'],
],
);
$result = $handler(new GetChildrenGradesSummaryQuery(
parentId: self::PARENT_ID,
tenantId: self::TENANT_ID,
));
self::assertCount(2, $result);
self::assertSame(16.0, $result[0]->generalAverage);
self::assertSame(8.0, $result[1]->generalAverage);
}
/**
* @param array<array{studentId: string, firstName: string, lastName: string}> $children
*/
private function createHandler(
array $children = [],
): GetChildrenGradesSummaryHandler {
$parentChildrenReader = new class($children) implements ParentChildrenReader {
/** @param array<array{studentId: string, firstName: string, lastName: string}> $children */
public function __construct(private readonly array $children)
{
}
public function childrenOf(string $guardianId, TenantId $tenantId): array
{
return $this->children;
}
};
$displayReader = new class implements ScheduleDisplayReader {
public function subjectDisplay(string $tenantId, string ...$subjectIds): array
{
$map = [];
foreach ($subjectIds as $id) {
$map[$id] = ['name' => 'Mathématiques', 'color' => '#3b82f6'];
}
return $map;
}
public function teacherNames(string $tenantId, string ...$teacherIds): array
{
return [];
}
};
$clock = new class($this->now) implements Clock {
public function __construct(private readonly DateTimeImmutable $now)
{
}
public function now(): DateTimeImmutable
{
return $this->now;
}
};
$delayReader = new class implements ParentGradeDelayReader {
public function delayHoursForTenant(TenantId $tenantId): int
{
return 24;
}
};
$gradesHandler = new GetChildrenGradesHandler(
$parentChildrenReader,
$this->evaluationRepository,
$this->gradeRepository,
$this->statisticsRepository,
$displayReader,
new VisibiliteNotesPolicy($clock),
$delayReader,
);
return new GetChildrenGradesSummaryHandler($gradesHandler);
}
protected function evaluationRepository(): InMemoryEvaluationRepository
{
return $this->evaluationRepository;
}
protected function gradeRepository(): InMemoryGradeRepository
{
return $this->gradeRepository;
}
}