createGrade(); self::assertSame(GradeStatus::GRADED, $grade->status); self::assertNotNull($grade->value); self::assertSame(15.5, $grade->value->value); } #[Test] public function saisirRecordsNoteSaisieEvent(): void { $grade = $this->createGrade(); $events = $grade->pullDomainEvents(); self::assertCount(1, $events); self::assertInstanceOf(NoteSaisie::class, $events[0]); self::assertSame($grade->id, $events[0]->gradeId); self::assertSame(15.5, $events[0]->value); self::assertSame('graded', $events[0]->status); } #[Test] public function saisirSetsAllProperties(): void { $tenantId = TenantId::fromString(self::TENANT_ID); $evaluationId = EvaluationId::fromString(self::EVALUATION_ID); $studentId = UserId::fromString(self::STUDENT_ID); $teacherId = UserId::fromString(self::TEACHER_ID); $now = new DateTimeImmutable('2026-03-27 10:00:00'); $value = new GradeValue(15.5); $gradeScale = new GradeScale(20); $grade = Grade::saisir( tenantId: $tenantId, evaluationId: $evaluationId, studentId: $studentId, value: $value, status: GradeStatus::GRADED, gradeScale: $gradeScale, createdBy: $teacherId, now: $now, ); self::assertTrue($grade->tenantId->equals($tenantId)); self::assertTrue($grade->evaluationId->equals($evaluationId)); self::assertTrue($grade->studentId->equals($studentId)); self::assertTrue($grade->createdBy->equals($teacherId)); self::assertSame(15.5, $grade->value->value); self::assertSame(GradeStatus::GRADED, $grade->status); self::assertEquals($now, $grade->createdAt); self::assertEquals($now, $grade->updatedAt); } #[Test] public function saisirCreatesAbsentGrade(): void { $grade = Grade::saisir( tenantId: TenantId::fromString(self::TENANT_ID), evaluationId: EvaluationId::fromString(self::EVALUATION_ID), studentId: UserId::fromString(self::STUDENT_ID), value: null, status: GradeStatus::ABSENT, gradeScale: new GradeScale(20), createdBy: UserId::fromString(self::TEACHER_ID), now: new DateTimeImmutable('2026-03-27 10:00:00'), ); self::assertSame(GradeStatus::ABSENT, $grade->status); self::assertNull($grade->value); } #[Test] public function saisirCreatesDispensedGrade(): void { $grade = Grade::saisir( tenantId: TenantId::fromString(self::TENANT_ID), evaluationId: EvaluationId::fromString(self::EVALUATION_ID), studentId: UserId::fromString(self::STUDENT_ID), value: null, status: GradeStatus::DISPENSED, gradeScale: new GradeScale(20), createdBy: UserId::fromString(self::TEACHER_ID), now: new DateTimeImmutable('2026-03-27 10:00:00'), ); self::assertSame(GradeStatus::DISPENSED, $grade->status); self::assertNull($grade->value); } #[Test] public function saisirThrowsWhenGradedWithoutValue(): void { $this->expectException(NoteRequiseException::class); Grade::saisir( tenantId: TenantId::fromString(self::TENANT_ID), evaluationId: EvaluationId::fromString(self::EVALUATION_ID), studentId: UserId::fromString(self::STUDENT_ID), value: null, status: GradeStatus::GRADED, gradeScale: new GradeScale(20), createdBy: UserId::fromString(self::TEACHER_ID), now: new DateTimeImmutable('2026-03-27 10:00:00'), ); } #[Test] public function saisirThrowsWhenValueExceedsGradeScale(): void { $this->expectException(ValeurNoteInvalideException::class); Grade::saisir( tenantId: TenantId::fromString(self::TENANT_ID), evaluationId: EvaluationId::fromString(self::EVALUATION_ID), studentId: UserId::fromString(self::STUDENT_ID), value: new GradeValue(25.0), status: GradeStatus::GRADED, gradeScale: new GradeScale(20), createdBy: UserId::fromString(self::TEACHER_ID), now: new DateTimeImmutable('2026-03-27 10:00:00'), ); } #[Test] public function modifierUpdatesValueAndRecordsEvent(): void { $grade = $this->createGrade(); $grade->pullDomainEvents(); $modifiedAt = new DateTimeImmutable('2026-03-27 14:00:00'); $modifierId = UserId::fromString(self::TEACHER_ID); $grade->modifier( value: new GradeValue(18.0), status: GradeStatus::GRADED, gradeScale: new GradeScale(20), modifiedBy: $modifierId, now: $modifiedAt, ); self::assertSame(18.0, $grade->value->value); self::assertEquals($modifiedAt, $grade->updatedAt); $events = $grade->pullDomainEvents(); self::assertCount(1, $events); self::assertInstanceOf(NoteModifiee::class, $events[0]); self::assertSame(15.5, $events[0]->oldValue); self::assertSame(18.0, $events[0]->newValue); self::assertSame('graded', $events[0]->oldStatus); self::assertSame('graded', $events[0]->newStatus); self::assertSame(self::TEACHER_ID, $events[0]->modifiedBy); } #[Test] public function modifierChangesToAbsent(): void { $grade = $this->createGrade(); $grade->pullDomainEvents(); $grade->modifier( value: null, status: GradeStatus::ABSENT, gradeScale: new GradeScale(20), modifiedBy: UserId::fromString(self::TEACHER_ID), now: new DateTimeImmutable('2026-03-27 14:00:00'), ); self::assertSame(GradeStatus::ABSENT, $grade->status); self::assertNull($grade->value); } #[Test] public function modifierThrowsWhenGradedWithoutValue(): void { $grade = $this->createGrade(); $this->expectException(NoteRequiseException::class); $grade->modifier( value: null, status: GradeStatus::GRADED, gradeScale: new GradeScale(20), modifiedBy: UserId::fromString(self::TEACHER_ID), now: new DateTimeImmutable('2026-03-27 14:00:00'), ); } #[Test] public function modifierThrowsWhenValueExceedsGradeScale(): void { $grade = $this->createGrade(); $this->expectException(ValeurNoteInvalideException::class); $grade->modifier( value: new GradeValue(25.0), status: GradeStatus::GRADED, gradeScale: new GradeScale(20), modifiedBy: UserId::fromString(self::TEACHER_ID), now: new DateTimeImmutable('2026-03-27 14:00:00'), ); } #[Test] public function reconstituteRestoresAllPropertiesWithoutEvents(): void { $gradeId = GradeId::generate(); $tenantId = TenantId::fromString(self::TENANT_ID); $evaluationId = EvaluationId::fromString(self::EVALUATION_ID); $studentId = UserId::fromString(self::STUDENT_ID); $teacherId = UserId::fromString(self::TEACHER_ID); $createdAt = new DateTimeImmutable('2026-03-27 10:00:00'); $updatedAt = new DateTimeImmutable('2026-03-27 14:00:00'); $value = new GradeValue(15.5); $grade = Grade::reconstitute( id: $gradeId, tenantId: $tenantId, evaluationId: $evaluationId, studentId: $studentId, value: $value, status: GradeStatus::GRADED, createdBy: $teacherId, createdAt: $createdAt, updatedAt: $updatedAt, ); self::assertTrue($grade->id->equals($gradeId)); self::assertTrue($grade->tenantId->equals($tenantId)); self::assertTrue($grade->evaluationId->equals($evaluationId)); self::assertTrue($grade->studentId->equals($studentId)); self::assertTrue($grade->createdBy->equals($teacherId)); self::assertSame(15.5, $grade->value->value); self::assertSame(GradeStatus::GRADED, $grade->status); self::assertEquals($createdAt, $grade->createdAt); self::assertEquals($updatedAt, $grade->updatedAt); self::assertEmpty($grade->pullDomainEvents()); } #[Test] public function saisirAppreciationSetsAppreciation(): void { $grade = $this->createGrade(); $now = new DateTimeImmutable('2026-03-31 10:00:00'); $grade->saisirAppreciation('Très bon travail', $now); self::assertSame('Très bon travail', $grade->appreciation); self::assertEquals($now, $grade->appreciationUpdatedAt); self::assertEquals($now, $grade->updatedAt); } #[Test] public function saisirAppreciationAcceptsNull(): void { $grade = $this->createGrade(); $grade->saisirAppreciation('Temporaire', new DateTimeImmutable('2026-03-31 09:00:00')); $grade->saisirAppreciation(null, new DateTimeImmutable('2026-03-31 10:00:00')); self::assertNull($grade->appreciation); } #[Test] public function saisirAppreciationAcceptsEmptyString(): void { $grade = $this->createGrade(); $grade->saisirAppreciation('Temporaire', new DateTimeImmutable('2026-03-31 09:00:00')); $grade->saisirAppreciation('', new DateTimeImmutable('2026-03-31 10:00:00')); self::assertNull($grade->appreciation); } #[Test] public function saisirAppreciationThrowsWhenTooLong(): void { $grade = $this->createGrade(); $this->expectException(AppreciationTropLongueException::class); $grade->saisirAppreciation(str_repeat('a', 501), new DateTimeImmutable('2026-03-31 10:00:00')); } #[Test] public function saisirAppreciationAcceptsMaxLength(): void { $grade = $this->createGrade(); $grade->saisirAppreciation(str_repeat('a', 500), new DateTimeImmutable('2026-03-31 10:00:00')); self::assertSame(500, mb_strlen($grade->appreciation ?? '')); } #[Test] public function newGradeHasNullAppreciation(): void { $grade = $this->createGrade(); self::assertNull($grade->appreciation); self::assertNull($grade->appreciationUpdatedAt); } #[Test] public function reconstituteRestoresAppreciation(): void { $gradeId = GradeId::generate(); $createdAt = new DateTimeImmutable('2026-03-27 10:00:00'); $updatedAt = new DateTimeImmutable('2026-03-27 14:00:00'); $appreciationUpdatedAt = new DateTimeImmutable('2026-03-31 10:00:00'); $grade = Grade::reconstitute( id: $gradeId, tenantId: TenantId::fromString(self::TENANT_ID), evaluationId: EvaluationId::fromString(self::EVALUATION_ID), studentId: UserId::fromString(self::STUDENT_ID), value: new GradeValue(15.5), status: GradeStatus::GRADED, createdBy: UserId::fromString(self::TEACHER_ID), createdAt: $createdAt, updatedAt: $updatedAt, appreciation: 'Bon travail', appreciationUpdatedAt: $appreciationUpdatedAt, ); self::assertSame('Bon travail', $grade->appreciation); self::assertEquals($appreciationUpdatedAt, $grade->appreciationUpdatedAt); } private function createGrade(): Grade { return Grade::saisir( tenantId: TenantId::fromString(self::TENANT_ID), evaluationId: EvaluationId::fromString(self::EVALUATION_ID), studentId: UserId::fromString(self::STUDENT_ID), value: new GradeValue(15.5), status: GradeStatus::GRADED, gradeScale: new GradeScale(20), createdBy: UserId::fromString(self::TEACHER_ID), now: new DateTimeImmutable('2026-03-27 10:00:00'), ); } }