evaluationRepository = new InMemoryEvaluationRepository(); $this->gradeRepository = new InMemoryGradeRepository(); $this->clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-03-27 10:00:00'); } }; $this->seedEvaluation(); } #[Test] public function itSavesNewGrades(): void { $handler = $this->createHandler(); $command = new SaveGradesCommand( tenantId: self::TENANT_ID, evaluationId: self::EVALUATION_ID, teacherId: self::TEACHER_ID, grades: [ ['studentId' => self::STUDENT_1_ID, 'value' => 15.5, 'status' => 'graded'], ['studentId' => self::STUDENT_2_ID, 'value' => 12.0, 'status' => 'graded'], ], ); $savedGrades = $handler($command); self::assertCount(2, $savedGrades); self::assertSame(15.5, $savedGrades[0]->value->value); self::assertSame(12.0, $savedGrades[1]->value->value); } #[Test] public function itPersistsGradesInRepository(): void { $handler = $this->createHandler(); $command = new SaveGradesCommand( tenantId: self::TENANT_ID, evaluationId: self::EVALUATION_ID, teacherId: self::TEACHER_ID, grades: [ ['studentId' => self::STUDENT_1_ID, 'value' => 15.5, 'status' => 'graded'], ], ); $handler($command); $tenantId = TenantId::fromString(self::TENANT_ID); $grades = $this->gradeRepository->findByEvaluation( EvaluationId::fromString(self::EVALUATION_ID), $tenantId, ); self::assertCount(1, $grades); self::assertSame(15.5, $grades[0]->value->value); } #[Test] public function itUpdatesExistingGrades(): void { $handler = $this->createHandler(); $handler(new SaveGradesCommand( tenantId: self::TENANT_ID, evaluationId: self::EVALUATION_ID, teacherId: self::TEACHER_ID, grades: [ ['studentId' => self::STUDENT_1_ID, 'value' => 15.5, 'status' => 'graded'], ], )); $handler(new SaveGradesCommand( tenantId: self::TENANT_ID, evaluationId: self::EVALUATION_ID, teacherId: self::TEACHER_ID, grades: [ ['studentId' => self::STUDENT_1_ID, 'value' => 18.0, 'status' => 'graded'], ], )); $tenantId = TenantId::fromString(self::TENANT_ID); $grades = $this->gradeRepository->findByEvaluation( EvaluationId::fromString(self::EVALUATION_ID), $tenantId, ); self::assertCount(1, $grades); self::assertSame(18.0, $grades[0]->value->value); } #[Test] public function itSavesAbsentGrade(): void { $handler = $this->createHandler(); $command = new SaveGradesCommand( tenantId: self::TENANT_ID, evaluationId: self::EVALUATION_ID, teacherId: self::TEACHER_ID, grades: [ ['studentId' => self::STUDENT_1_ID, 'value' => null, 'status' => 'absent'], ], ); $savedGrades = $handler($command); self::assertSame(GradeStatus::ABSENT, $savedGrades[0]->status); self::assertNull($savedGrades[0]->value); } #[Test] public function itSavesDispensedGrade(): void { $handler = $this->createHandler(); $command = new SaveGradesCommand( tenantId: self::TENANT_ID, evaluationId: self::EVALUATION_ID, teacherId: self::TEACHER_ID, grades: [ ['studentId' => self::STUDENT_1_ID, 'value' => null, 'status' => 'dispensed'], ], ); $savedGrades = $handler($command); self::assertSame(GradeStatus::DISPENSED, $savedGrades[0]->status); self::assertNull($savedGrades[0]->value); } #[Test] public function itThrowsWhenTeacherNotOwner(): void { $handler = $this->createHandler(); $otherTeacher = '550e8400-e29b-41d4-a716-446655440099'; $this->expectException(NonProprietaireDeLEvaluationException::class); $handler(new SaveGradesCommand( tenantId: self::TENANT_ID, evaluationId: self::EVALUATION_ID, teacherId: $otherTeacher, grades: [ ['studentId' => self::STUDENT_1_ID, 'value' => 15.5, 'status' => 'graded'], ], )); } #[Test] public function itThrowsWhenValueExceedsGradeScale(): void { $handler = $this->createHandler(); $this->expectException(ValeurNoteInvalideException::class); $handler(new SaveGradesCommand( tenantId: self::TENANT_ID, evaluationId: self::EVALUATION_ID, teacherId: self::TEACHER_ID, grades: [ ['studentId' => self::STUDENT_1_ID, 'value' => 25.0, 'status' => 'graded'], ], )); } #[Test] public function itThrowsWhenGradedWithoutValue(): void { $handler = $this->createHandler(); $this->expectException(NoteRequiseException::class); $handler(new SaveGradesCommand( tenantId: self::TENANT_ID, evaluationId: self::EVALUATION_ID, teacherId: self::TEACHER_ID, grades: [ ['studentId' => self::STUDENT_1_ID, 'value' => null, 'status' => 'graded'], ], )); } private function createHandler(): SaveGradesHandler { return new SaveGradesHandler( $this->evaluationRepository, $this->gradeRepository, $this->clock, ); } private function seedEvaluation(): void { $evaluation = Evaluation::reconstitute( id: EvaluationId::fromString(self::EVALUATION_ID), tenantId: TenantId::fromString(self::TENANT_ID), classId: ClassId::fromString('550e8400-e29b-41d4-a716-446655440020'), subjectId: SubjectId::fromString('550e8400-e29b-41d4-a716-446655440030'), teacherId: UserId::fromString(self::TEACHER_ID), title: 'ContrĂ´le chapitre 5', description: null, evaluationDate: new DateTimeImmutable('2026-04-15'), gradeScale: new GradeScale(20), coefficient: new Coefficient(1.0), status: EvaluationStatus::PUBLISHED, createdAt: new DateTimeImmutable('2026-03-12 10:00:00'), updatedAt: new DateTimeImmutable('2026-03-12 10:00:00'), ); $this->evaluationRepository->save($evaluation); } }