feat: Permettre aux enseignants de contourner les règles de devoirs avec justification
Akeneo permet de configurer des règles de devoirs en mode Hard qui bloquent totalement la création. Or certains cas légitimes (sorties scolaires, événements exceptionnels) nécessitent de passer outre ces règles. Sans mécanisme d'exception, l'enseignant est bloqué et doit contacter manuellement la direction. Cette implémentation ajoute un flux complet d'exception : l'enseignant justifie sa demande (min 20 caractères), le devoir est créé immédiatement, et la direction est notifiée par email. Le handler vérifie côté serveur que les règles sont réellement bloquantes avant d'accepter l'exception, empêchant toute fabrication de fausses exceptions via l'API. La direction dispose d'un rapport filtrable par période, enseignant et type de règle.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Scolarite\Domain\Model\Homework;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Event\ExceptionDevoirDemandee;
|
||||
use App\Scolarite\Domain\Exception\JustificationTropCourteException;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkRuleException;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkRuleExceptionId;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
use function str_repeat;
|
||||
|
||||
final class HomeworkRuleExceptionTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string HOMEWORK_ID = '550e8400-e29b-41d4-a716-446655440050';
|
||||
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
|
||||
#[Test]
|
||||
public function demanderCreatesExceptionWithAllFields(): void
|
||||
{
|
||||
$tenantId = TenantId::fromString(self::TENANT_ID);
|
||||
$homeworkId = HomeworkId::fromString(self::HOMEWORK_ID);
|
||||
$teacherId = UserId::fromString(self::TEACHER_ID);
|
||||
$now = new DateTimeImmutable('2026-03-19 10:00:00');
|
||||
|
||||
$exception = HomeworkRuleException::demander(
|
||||
tenantId: $tenantId,
|
||||
homeworkId: $homeworkId,
|
||||
ruleTypes: ['minimum_delay'],
|
||||
justification: 'Sortie scolaire prévue, les élèves doivent préparer leur dossier.',
|
||||
createdBy: $teacherId,
|
||||
now: $now,
|
||||
);
|
||||
|
||||
self::assertNotEmpty((string) $exception->id);
|
||||
self::assertTrue($exception->tenantId->equals($tenantId));
|
||||
self::assertTrue($exception->homeworkId->equals($homeworkId));
|
||||
self::assertSame('minimum_delay', $exception->ruleType);
|
||||
self::assertSame(['minimum_delay'], $exception->ruleTypes());
|
||||
self::assertSame('Sortie scolaire prévue, les élèves doivent préparer leur dossier.', $exception->justification);
|
||||
self::assertTrue($exception->createdBy->equals($teacherId));
|
||||
self::assertEquals($now, $exception->createdAt);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function demanderStoresMultipleRuleTypesCommaSeparated(): void
|
||||
{
|
||||
$exception = HomeworkRuleException::demander(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
homeworkId: HomeworkId::fromString(self::HOMEWORK_ID),
|
||||
ruleTypes: ['minimum_delay', 'no_monday_after'],
|
||||
justification: 'Sortie scolaire et lundi exceptionnel.',
|
||||
createdBy: UserId::fromString(self::TEACHER_ID),
|
||||
now: new DateTimeImmutable('2026-03-19 10:00:00'),
|
||||
);
|
||||
|
||||
self::assertSame('minimum_delay,no_monday_after', $exception->ruleType);
|
||||
self::assertSame(['minimum_delay', 'no_monday_after'], $exception->ruleTypes());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function demanderRecordsExceptionDevoirDemandeeEvent(): void
|
||||
{
|
||||
$exception = $this->createException();
|
||||
|
||||
$events = $exception->pullDomainEvents();
|
||||
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(ExceptionDevoirDemandee::class, $events[0]);
|
||||
self::assertSame($exception->id, $events[0]->exceptionId);
|
||||
self::assertSame(['minimum_delay'], $events[0]->ruleTypes);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function demanderThrowsWhenJustificationTooShort(): void
|
||||
{
|
||||
$this->expectException(JustificationTropCourteException::class);
|
||||
$this->expectExceptionMessage('au moins 20 caractères');
|
||||
|
||||
HomeworkRuleException::demander(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
homeworkId: HomeworkId::fromString(self::HOMEWORK_ID),
|
||||
ruleTypes: ['minimum_delay'],
|
||||
justification: 'Trop court',
|
||||
createdBy: UserId::fromString(self::TEACHER_ID),
|
||||
now: new DateTimeImmutable('2026-03-19 10:00:00'),
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function demanderAcceptsExactlyMinimumLength(): void
|
||||
{
|
||||
$justification = str_repeat('x', 20);
|
||||
|
||||
$exception = HomeworkRuleException::demander(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
homeworkId: HomeworkId::fromString(self::HOMEWORK_ID),
|
||||
ruleTypes: ['minimum_delay'],
|
||||
justification: $justification,
|
||||
createdBy: UserId::fromString(self::TEACHER_ID),
|
||||
now: new DateTimeImmutable('2026-03-19 10:00:00'),
|
||||
);
|
||||
|
||||
self::assertSame($justification, $exception->justification);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function demanderRejectsNineteenCharacters(): void
|
||||
{
|
||||
$this->expectException(JustificationTropCourteException::class);
|
||||
|
||||
HomeworkRuleException::demander(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
homeworkId: HomeworkId::fromString(self::HOMEWORK_ID),
|
||||
ruleTypes: ['minimum_delay'],
|
||||
justification: str_repeat('x', 19),
|
||||
createdBy: UserId::fromString(self::TEACHER_ID),
|
||||
now: new DateTimeImmutable('2026-03-19 10:00:00'),
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function reconstituteRestoresAllPropertiesWithoutEvents(): void
|
||||
{
|
||||
$id = HomeworkRuleExceptionId::generate();
|
||||
$tenantId = TenantId::fromString(self::TENANT_ID);
|
||||
$homeworkId = HomeworkId::fromString(self::HOMEWORK_ID);
|
||||
$teacherId = UserId::fromString(self::TEACHER_ID);
|
||||
$createdAt = new DateTimeImmutable('2026-03-19 10:00:00');
|
||||
|
||||
$exception = HomeworkRuleException::reconstitute(
|
||||
id: $id,
|
||||
tenantId: $tenantId,
|
||||
homeworkId: $homeworkId,
|
||||
ruleType: 'minimum_delay',
|
||||
justification: 'Justification suffisamment longue pour passer.',
|
||||
createdBy: $teacherId,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
self::assertTrue($exception->id->equals($id));
|
||||
self::assertTrue($exception->tenantId->equals($tenantId));
|
||||
self::assertTrue($exception->homeworkId->equals($homeworkId));
|
||||
self::assertSame('minimum_delay', $exception->ruleType);
|
||||
self::assertSame('Justification suffisamment longue pour passer.', $exception->justification);
|
||||
self::assertTrue($exception->createdBy->equals($teacherId));
|
||||
self::assertEquals($createdAt, $exception->createdAt);
|
||||
self::assertEmpty($exception->pullDomainEvents());
|
||||
}
|
||||
|
||||
private function createException(): HomeworkRuleException
|
||||
{
|
||||
return HomeworkRuleException::demander(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
homeworkId: HomeworkId::fromString(self::HOMEWORK_ID),
|
||||
ruleTypes: ['minimum_delay'],
|
||||
justification: 'Sortie scolaire prévue, les élèves doivent préparer leur dossier.',
|
||||
createdBy: UserId::fromString(self::TEACHER_ID),
|
||||
now: new DateTimeImmutable('2026-03-19 10:00:00'),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user