repository = new InMemoryGradingConfigurationRepository(); $this->clock = new class implements Clock { #[Override] public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-02-01 10:00:00'); } }; } #[Test] public function itCreatesNewConfigurationWhenNoneExists(): void { $handler = $this->createHandler(hasGrades: false); $result = $handler(new ConfigureGradingModeCommand( tenantId: self::TENANT_ID, schoolId: self::SCHOOL_ID, academicYearId: self::ACADEMIC_YEAR_ID, gradingMode: 'numeric_20', )); self::assertSame(GradingMode::NUMERIC_20, $result->gradingConfiguration->mode); } #[Test] public function itPersistsConfigurationInRepository(): void { $handler = $this->createHandler(hasGrades: false); $handler(new ConfigureGradingModeCommand( tenantId: self::TENANT_ID, schoolId: self::SCHOOL_ID, academicYearId: self::ACADEMIC_YEAR_ID, gradingMode: 'competencies', )); $found = $this->repository->findBySchoolAndYear( TenantId::fromString(self::TENANT_ID), SchoolId::fromString(self::SCHOOL_ID), AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), ); self::assertNotNull($found); self::assertSame(GradingMode::COMPETENCIES, $found->gradingConfiguration->mode); } #[Test] public function itChangesExistingModeWhenNoGradesExist(): void { $handler = $this->createHandler(hasGrades: false); $handler(new ConfigureGradingModeCommand( tenantId: self::TENANT_ID, schoolId: self::SCHOOL_ID, academicYearId: self::ACADEMIC_YEAR_ID, gradingMode: 'numeric_20', )); $result = $handler(new ConfigureGradingModeCommand( tenantId: self::TENANT_ID, schoolId: self::SCHOOL_ID, academicYearId: self::ACADEMIC_YEAR_ID, gradingMode: 'letters', )); self::assertSame(GradingMode::LETTERS, $result->gradingConfiguration->mode); } #[Test] public function itBlocksChangeWhenGradesExist(): void { $handlerNoGrades = $this->createHandler(hasGrades: false); $handlerNoGrades(new ConfigureGradingModeCommand( tenantId: self::TENANT_ID, schoolId: self::SCHOOL_ID, academicYearId: self::ACADEMIC_YEAR_ID, gradingMode: 'numeric_20', )); $handlerWithGrades = $this->createHandler(hasGrades: true); $this->expectException(CannotChangeGradingModeWithExistingGradesException::class); $handlerWithGrades(new ConfigureGradingModeCommand( tenantId: self::TENANT_ID, schoolId: self::SCHOOL_ID, academicYearId: self::ACADEMIC_YEAR_ID, gradingMode: 'competencies', )); } #[Test] public function itAllowsSameModeEvenWithGradesExisting(): void { $handlerNoGrades = $this->createHandler(hasGrades: false); $handlerNoGrades(new ConfigureGradingModeCommand( tenantId: self::TENANT_ID, schoolId: self::SCHOOL_ID, academicYearId: self::ACADEMIC_YEAR_ID, gradingMode: 'numeric_20', )); $handlerWithGrades = $this->createHandler(hasGrades: true); $result = $handlerWithGrades(new ConfigureGradingModeCommand( tenantId: self::TENANT_ID, schoolId: self::SCHOOL_ID, academicYearId: self::ACADEMIC_YEAR_ID, gradingMode: 'numeric_20', )); self::assertSame(GradingMode::NUMERIC_20, $result->gradingConfiguration->mode); } #[Test] public function itBlocksInitialNonDefaultModeWhenGradesExist(): void { $handler = $this->createHandler(hasGrades: true); $this->expectException(CannotChangeGradingModeWithExistingGradesException::class); $handler(new ConfigureGradingModeCommand( tenantId: self::TENANT_ID, schoolId: self::SCHOOL_ID, academicYearId: self::ACADEMIC_YEAR_ID, gradingMode: 'competencies', )); } #[Test] public function itIsolatesConfigurationByTenant(): void { $handler = $this->createHandler(hasGrades: false); $handler(new ConfigureGradingModeCommand( tenantId: self::TENANT_ID, schoolId: self::SCHOOL_ID, academicYearId: self::ACADEMIC_YEAR_ID, gradingMode: 'numeric_20', )); $otherTenantId = '550e8400-e29b-41d4-a716-446655440099'; $handler(new ConfigureGradingModeCommand( tenantId: $otherTenantId, schoolId: self::SCHOOL_ID, academicYearId: self::ACADEMIC_YEAR_ID, gradingMode: 'competencies', )); $config1 = $this->repository->findBySchoolAndYear( TenantId::fromString(self::TENANT_ID), SchoolId::fromString(self::SCHOOL_ID), AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), ); $config2 = $this->repository->findBySchoolAndYear( TenantId::fromString($otherTenantId), SchoolId::fromString(self::SCHOOL_ID), AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), ); self::assertNotNull($config1); self::assertNotNull($config2); self::assertSame(GradingMode::NUMERIC_20, $config1->gradingConfiguration->mode); self::assertSame(GradingMode::COMPETENCIES, $config2->gradingConfiguration->mode); } private function createHandler(bool $hasGrades): ConfigureGradingModeHandler { $gradeChecker = new class($hasGrades) implements GradeExistenceChecker { public function __construct(private bool $hasGrades) { } #[Override] public function hasGradesInPeriod(TenantId $tenantId, AcademicYearId $academicYearId, int $periodSequence): bool { return $this->hasGrades; } #[Override] public function hasGradesForYear(TenantId $tenantId, SchoolId $schoolId, AcademicYearId $academicYearId): bool { return $this->hasGrades; } }; return new ConfigureGradingModeHandler( $this->repository, $gradeChecker, $this->clock, ); } }