Lorsqu'un super-admin crée un établissement via l'interface, le système doit automatiquement créer la base tenant, exécuter les migrations, créer le premier utilisateur admin et envoyer l'invitation — le tout de manière asynchrone pour ne pas bloquer la réponse HTTP. Ce mécanisme rend chaque établissement opérationnel dès sa création sans intervention manuelle sur l'infrastructure.
250 lines
8.9 KiB
PHP
250 lines
8.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Scolarite\Application\Service;
|
|
|
|
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
|
use App\Administration\Domain\Model\Subject\SubjectId;
|
|
use App\Administration\Domain\Model\User\UserId;
|
|
use App\Scolarite\Application\Port\EnseignantAffectationChecker;
|
|
use App\Scolarite\Application\Service\AutorisationSaisieNotesChecker;
|
|
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
|
|
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
|
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
|
|
use App\Scolarite\Domain\Model\TeacherReplacement\ClassSubjectPair;
|
|
use App\Scolarite\Domain\Model\TeacherReplacement\TeacherReplacement;
|
|
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryTeacherReplacementRepository;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class AutorisationSaisieNotesCheckerTest extends TestCase
|
|
{
|
|
private TenantId $tenantId;
|
|
private ClassId $classId;
|
|
private SubjectId $subjectId;
|
|
private InMemoryTeacherReplacementRepository $replacementRepository;
|
|
private DateTimeImmutable $now;
|
|
|
|
/** @var array<string, bool> */
|
|
private array $affectationResults = [];
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->tenantId = TenantId::generate();
|
|
$this->classId = ClassId::generate();
|
|
$this->subjectId = SubjectId::generate();
|
|
$this->replacementRepository = new InMemoryTeacherReplacementRepository();
|
|
$this->now = new DateTimeImmutable('2026-04-14 10:00:00');
|
|
}
|
|
|
|
#[Test]
|
|
public function itReturnsTrueWhenTeacherIsAffected(): void
|
|
{
|
|
$teacherId = UserId::generate();
|
|
$this->setTeacherAffecte($teacherId);
|
|
|
|
$checker = $this->createChecker();
|
|
$evaluation = $this->createEvaluation();
|
|
|
|
self::assertTrue($checker->peutSaisirNotes($teacherId, $evaluation, $this->tenantId, $this->now));
|
|
}
|
|
|
|
#[Test]
|
|
public function itReturnsFalseWhenTeacherIsNotAffectedAndNoReplacement(): void
|
|
{
|
|
$teacherId = UserId::generate();
|
|
|
|
$checker = $this->createChecker();
|
|
$evaluation = $this->createEvaluation();
|
|
|
|
self::assertFalse($checker->peutSaisirNotes($teacherId, $evaluation, $this->tenantId, $this->now));
|
|
}
|
|
|
|
#[Test]
|
|
public function itReturnsTrueWhenTeacherHasActiveReplacement(): void
|
|
{
|
|
$replacementTeacherId = UserId::generate();
|
|
$this->createActiveReplacement($replacementTeacherId);
|
|
|
|
$checker = $this->createChecker();
|
|
$evaluation = $this->createEvaluation();
|
|
|
|
self::assertTrue($checker->peutSaisirNotes($replacementTeacherId, $evaluation, $this->tenantId, $this->now));
|
|
}
|
|
|
|
#[Test]
|
|
public function itReturnsFalseWhenReplacementIsExpired(): void
|
|
{
|
|
$replacementTeacherId = UserId::generate();
|
|
$this->createExpiredReplacement($replacementTeacherId);
|
|
|
|
$checker = $this->createChecker();
|
|
$evaluation = $this->createEvaluation();
|
|
|
|
self::assertFalse($checker->peutSaisirNotes($replacementTeacherId, $evaluation, $this->tenantId, $this->now));
|
|
}
|
|
|
|
#[Test]
|
|
public function itReturnsTrueWhenBothAffectedAndActiveReplacement(): void
|
|
{
|
|
$teacherId = UserId::generate();
|
|
$this->setTeacherAffecte($teacherId);
|
|
$this->createActiveReplacement($teacherId);
|
|
|
|
$checker = $this->createChecker();
|
|
$evaluation = $this->createEvaluation();
|
|
|
|
self::assertTrue($checker->peutSaisirNotes($teacherId, $evaluation, $this->tenantId, $this->now));
|
|
}
|
|
|
|
#[Test]
|
|
public function itReturnsFalseWhenReplacementIsOnDifferentClassSubject(): void
|
|
{
|
|
$replacementTeacherId = UserId::generate();
|
|
|
|
$otherClassId = ClassId::generate();
|
|
$otherSubjectId = SubjectId::generate();
|
|
$replacement = TeacherReplacement::designer(
|
|
tenantId: $this->tenantId,
|
|
replacedTeacherId: UserId::generate(),
|
|
replacementTeacherId: $replacementTeacherId,
|
|
startDate: $this->now->modify('-1 day'),
|
|
endDate: $this->now->modify('+7 days'),
|
|
classes: [new ClassSubjectPair($otherClassId, $otherSubjectId)],
|
|
reason: 'Maladie',
|
|
createdBy: UserId::generate(),
|
|
now: $this->now->modify('-1 day'),
|
|
);
|
|
$this->replacementRepository->save($replacement);
|
|
|
|
$checker = $this->createChecker();
|
|
$evaluation = $this->createEvaluation();
|
|
|
|
self::assertFalse($checker->peutSaisirNotes($replacementTeacherId, $evaluation, $this->tenantId, $this->now));
|
|
}
|
|
|
|
#[Test]
|
|
public function itReturnsFalseWhenReplacementStartsInTheFuture(): void
|
|
{
|
|
$replacementTeacherId = UserId::generate();
|
|
$futureStart = $this->now->modify('+1 day');
|
|
|
|
$replacement = TeacherReplacement::designer(
|
|
tenantId: $this->tenantId,
|
|
replacedTeacherId: UserId::generate(),
|
|
replacementTeacherId: $replacementTeacherId,
|
|
startDate: $futureStart,
|
|
endDate: $this->now->modify('+14 days'),
|
|
classes: [new ClassSubjectPair($this->classId, $this->subjectId)],
|
|
reason: 'Congé prévu',
|
|
createdBy: UserId::generate(),
|
|
now: $this->now,
|
|
);
|
|
$this->replacementRepository->save($replacement);
|
|
|
|
$checker = $this->createChecker();
|
|
$evaluation = $this->createEvaluation();
|
|
|
|
self::assertFalse($checker->peutSaisirNotes($replacementTeacherId, $evaluation, $this->tenantId, $this->now));
|
|
}
|
|
|
|
#[Test]
|
|
public function itShortCircuitsOnAffectationWithoutCheckingReplacement(): void
|
|
{
|
|
$teacherId = UserId::generate();
|
|
$this->setTeacherAffecte($teacherId);
|
|
|
|
// No replacement seeded — if it tries to check replacement for an
|
|
// assigned teacher, the result should still be true (short-circuit)
|
|
$checker = $this->createChecker();
|
|
$evaluation = $this->createEvaluation();
|
|
|
|
self::assertTrue($checker->peutSaisirNotes($teacherId, $evaluation, $this->tenantId, $this->now));
|
|
}
|
|
|
|
private function setTeacherAffecte(UserId $teacherId): void
|
|
{
|
|
$this->affectationResults[(string) $teacherId] = true;
|
|
}
|
|
|
|
private function createChecker(): AutorisationSaisieNotesChecker
|
|
{
|
|
$test = $this;
|
|
$affectationChecker = new class($test) implements EnseignantAffectationChecker {
|
|
public function __construct(private readonly AutorisationSaisieNotesCheckerTest $test)
|
|
{
|
|
}
|
|
|
|
public function estAffecte(
|
|
UserId $teacherId,
|
|
ClassId $classId,
|
|
SubjectId $subjectId,
|
|
TenantId $tenantId,
|
|
): bool {
|
|
return $this->test->isTeacherAffecte((string) $teacherId);
|
|
}
|
|
};
|
|
|
|
return new AutorisationSaisieNotesChecker(
|
|
$affectationChecker,
|
|
$this->replacementRepository,
|
|
);
|
|
}
|
|
|
|
public function isTeacherAffecte(string $teacherId): bool
|
|
{
|
|
return $this->affectationResults[$teacherId] ?? false;
|
|
}
|
|
|
|
private function createEvaluation(): Evaluation
|
|
{
|
|
return Evaluation::creer(
|
|
tenantId: $this->tenantId,
|
|
classId: $this->classId,
|
|
subjectId: $this->subjectId,
|
|
teacherId: UserId::generate(),
|
|
title: 'Contrôle de maths',
|
|
description: null,
|
|
evaluationDate: $this->now,
|
|
gradeScale: new GradeScale(20),
|
|
coefficient: new Coefficient(1.0),
|
|
now: $this->now,
|
|
);
|
|
}
|
|
|
|
private function createActiveReplacement(UserId $replacementTeacherId): void
|
|
{
|
|
$replacement = TeacherReplacement::designer(
|
|
tenantId: $this->tenantId,
|
|
replacedTeacherId: UserId::generate(),
|
|
replacementTeacherId: $replacementTeacherId,
|
|
startDate: $this->now->modify('-1 day'),
|
|
endDate: $this->now->modify('+7 days'),
|
|
classes: [new ClassSubjectPair($this->classId, $this->subjectId)],
|
|
reason: 'Maladie',
|
|
createdBy: UserId::generate(),
|
|
now: $this->now->modify('-1 day'),
|
|
);
|
|
$this->replacementRepository->save($replacement);
|
|
}
|
|
|
|
private function createExpiredReplacement(UserId $replacementTeacherId): void
|
|
{
|
|
$replacement = TeacherReplacement::designer(
|
|
tenantId: $this->tenantId,
|
|
replacedTeacherId: UserId::generate(),
|
|
replacementTeacherId: $replacementTeacherId,
|
|
startDate: $this->now->modify('-14 days'),
|
|
endDate: $this->now->modify('-1 day'),
|
|
classes: [new ClassSubjectPair($this->classId, $this->subjectId)],
|
|
reason: 'Maladie',
|
|
createdBy: UserId::generate(),
|
|
now: $this->now->modify('-14 days'),
|
|
);
|
|
$this->replacementRepository->save($replacement);
|
|
}
|
|
}
|