feat: Avertir l'enseignant quand un devoir ne respecte pas les règles (mode soft)
Quand un établissement configure des règles de devoirs en mode "soft", l'enseignant est maintenant averti avant la création si la date d'échéance ne respecte pas les contraintes (délai minimum, pas de lundi après un certain créneau). Il peut alors choisir de continuer (avec traçabilité) ou de modifier la date vers une date conforme. Le mode "hard" (blocage) reste protégé : acknowledgeWarning ne permet pas de contourner les règles bloquantes, préparant la story 5.5.
This commit is contained in:
@@ -16,7 +16,11 @@ use App\Scolarite\Domain\Repository\HomeworkRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
/**
|
||||
* Tests for homework API endpoints.
|
||||
@@ -53,6 +57,7 @@ final class HomeworkEndpointsTest extends ApiTestCase
|
||||
/** @var Connection $connection */
|
||||
$connection = $container->get(Connection::class);
|
||||
$connection->executeStatement('DELETE FROM homework WHERE tenant_id = :tid', ['tid' => self::TENANT_ID]);
|
||||
$connection->executeStatement('DELETE FROM homework_rules WHERE tenant_id = :tid', ['tid' => self::TENANT_ID]);
|
||||
$connection->executeStatement('DELETE FROM school_classes WHERE id IN (:id1, :id2)', ['id1' => self::CLASS_ID, 'id2' => self::TARGET_CLASS_ID]);
|
||||
$connection->executeStatement('DELETE FROM subjects WHERE id = :id', ['id' => self::SUBJECT_ID]);
|
||||
$connection->executeStatement('DELETE FROM users WHERE id IN (:o, :t)', ['o' => self::OWNER_TEACHER_ID, 't' => self::OTHER_TEACHER_ID]);
|
||||
@@ -506,6 +511,118 @@ final class HomeworkEndpointsTest extends ApiTestCase
|
||||
self::assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 5.4-FUNC-001 (P2) - GET /homework/{id} with rule override -> hasRuleOverride = true
|
||||
// =========================================================================
|
||||
|
||||
#[Test]
|
||||
public function getHomeworkShowsHasRuleOverrideTrue(): void
|
||||
{
|
||||
$this->persistHomeworkWithRuleOverride(self::HOMEWORK_ID);
|
||||
|
||||
$client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']);
|
||||
|
||||
$client->request('GET', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID, [
|
||||
'headers' => [
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(200);
|
||||
self::assertJsonContains(['hasRuleOverride' => true]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 5.4-FUNC-002 (P2) - GET /homework/{id} without rule override -> hasRuleOverride = false
|
||||
// =========================================================================
|
||||
|
||||
#[Test]
|
||||
public function getHomeworkShowsHasRuleOverrideFalse(): void
|
||||
{
|
||||
$this->persistHomework(self::HOMEWORK_ID, HomeworkStatus::PUBLISHED);
|
||||
|
||||
$client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']);
|
||||
|
||||
$client->request('GET', 'http://ecole-alpha.classeo.local/api/homework/' . self::HOMEWORK_ID, [
|
||||
'headers' => [
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(200);
|
||||
self::assertJsonContains(['hasRuleOverride' => false]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 5.4-FUNC-003 (P1) - POST /homework with soft rules violated → 409 with warnings
|
||||
// =========================================================================
|
||||
|
||||
#[Test]
|
||||
public function createHomeworkReturns409WhenSoftRulesViolated(): void
|
||||
{
|
||||
$this->persistSoftRulesWithMinimumDelay(7);
|
||||
$this->seedTeacherAssignment();
|
||||
|
||||
$client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']);
|
||||
|
||||
// Due date tomorrow = violates 7-day minimum_delay
|
||||
$tomorrow = (new DateTimeImmutable('+1 weekday'))->format('Y-m-d');
|
||||
|
||||
$client->request('POST', 'http://ecole-alpha.classeo.local/api/homework', [
|
||||
'headers' => [
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'json' => [
|
||||
'classId' => self::CLASS_ID,
|
||||
'subjectId' => self::SUBJECT_ID,
|
||||
'title' => 'Devoir test 409',
|
||||
'dueDate' => $tomorrow,
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(409);
|
||||
$json = $client->getResponse()->toArray(false);
|
||||
self::assertSame('homework_rules_warning', $json['type']);
|
||||
self::assertNotEmpty($json['warnings']);
|
||||
self::assertSame('minimum_delay', $json['warnings'][0]['ruleType']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 5.4-FUNC-004 (P1) - POST /homework with acknowledgeWarning → 201 created
|
||||
// =========================================================================
|
||||
|
||||
#[Test]
|
||||
public function createHomeworkReturns201WhenSoftRulesAcknowledged(): void
|
||||
{
|
||||
$this->persistSoftRulesWithMinimumDelay(7);
|
||||
$this->seedTeacherAssignment();
|
||||
|
||||
$client = $this->createAuthenticatedClient(self::OWNER_TEACHER_ID, ['ROLE_PROF']);
|
||||
|
||||
$tomorrow = (new DateTimeImmutable('+1 weekday'))->format('Y-m-d');
|
||||
|
||||
$client->request('POST', 'http://ecole-alpha.classeo.local/api/homework', [
|
||||
'headers' => [
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'json' => [
|
||||
'classId' => self::CLASS_ID,
|
||||
'subjectId' => self::SUBJECT_ID,
|
||||
'title' => 'Devoir acknowledge',
|
||||
'dueDate' => $tomorrow,
|
||||
'acknowledgeWarning' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
self::assertJsonContains([
|
||||
'title' => 'Devoir acknowledge',
|
||||
'hasRuleOverride' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helpers
|
||||
// =========================================================================
|
||||
@@ -527,6 +644,71 @@ final class HomeworkEndpointsTest extends ApiTestCase
|
||||
return $client;
|
||||
}
|
||||
|
||||
private function persistSoftRulesWithMinimumDelay(int $days): void
|
||||
{
|
||||
/** @var Connection $connection */
|
||||
$connection = static::getContainer()->get(Connection::class);
|
||||
$rulesJson = json_encode([['type' => 'minimum_delay', 'params' => ['days' => $days]]], JSON_THROW_ON_ERROR);
|
||||
$connection->executeStatement(
|
||||
"INSERT INTO homework_rules (id, tenant_id, rules, enforcement_mode, enabled, created_at, updated_at)
|
||||
VALUES (gen_random_uuid(), :tid, :rules::jsonb, 'soft', true, NOW(), NOW())
|
||||
ON CONFLICT (tenant_id) DO UPDATE SET rules = :rules::jsonb, enforcement_mode = 'soft', enabled = true, updated_at = NOW()",
|
||||
['tid' => self::TENANT_ID, 'rules' => $rulesJson],
|
||||
);
|
||||
}
|
||||
|
||||
private function seedTeacherAssignment(): void
|
||||
{
|
||||
/** @var Connection $connection */
|
||||
$connection = static::getContainer()->get(Connection::class);
|
||||
|
||||
// Compute the current academic year UUID the same way CurrentAcademicYearResolver does
|
||||
$month = (int) date('n');
|
||||
$year = (int) date('Y');
|
||||
$startYear = $month >= 9 ? $year : $year - 1;
|
||||
$academicYearId = Uuid::uuid5(
|
||||
'6ba7b814-9dad-11d1-80b4-00c04fd430c8',
|
||||
self::TENANT_ID . ':' . $startYear . '-' . ($startYear + 1),
|
||||
)->toString();
|
||||
|
||||
$connection->executeStatement(
|
||||
"INSERT INTO teacher_assignments (id, tenant_id, teacher_id, school_class_id, subject_id, academic_year_id, status, start_date, created_at, updated_at)
|
||||
VALUES (gen_random_uuid(), :tid, :teacher, :class, :subject, :ayid, 'active', NOW(), NOW(), NOW())
|
||||
ON CONFLICT DO NOTHING",
|
||||
[
|
||||
'tid' => self::TENANT_ID,
|
||||
'teacher' => self::OWNER_TEACHER_ID,
|
||||
'class' => self::CLASS_ID,
|
||||
'subject' => self::SUBJECT_ID,
|
||||
'ayid' => $academicYearId,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
private function persistHomeworkWithRuleOverride(string $homeworkId): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
|
||||
$homework = Homework::reconstitute(
|
||||
id: HomeworkId::fromString($homeworkId),
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
classId: ClassId::fromString(self::CLASS_ID),
|
||||
subjectId: SubjectId::fromString(self::SUBJECT_ID),
|
||||
teacherId: UserId::fromString(self::OWNER_TEACHER_ID),
|
||||
title: 'Devoir existant',
|
||||
description: null,
|
||||
dueDate: new DateTimeImmutable('2026-06-15'),
|
||||
status: HomeworkStatus::PUBLISHED,
|
||||
createdAt: $now,
|
||||
updatedAt: $now,
|
||||
ruleOverride: ['warnings' => ['minimum_delay'], 'acknowledgedAt' => '2026-03-18T10:00:00+00:00'],
|
||||
);
|
||||
|
||||
/** @var HomeworkRepository $repository */
|
||||
$repository = static::getContainer()->get(HomeworkRepository::class);
|
||||
$repository->save($homework);
|
||||
}
|
||||
|
||||
private function persistHomework(string $homeworkId, HomeworkStatus $status): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
|
||||
Reference in New Issue
Block a user