feat: Liaison parents-enfants avec gestion des tuteurs

Les parents doivent pouvoir suivre la scolarité de leurs enfants (notes,
emploi du temps, devoirs). Cela nécessite un lien formalisé entre le
compte parent et le compte élève, géré par les administrateurs.

Le lien est établi soit manuellement via l'interface d'administration,
soit automatiquement lors de l'activation du compte parent lorsque
l'invitation inclut un élève cible. Ce lien conditionne l'accès aux
données scolaires de l'enfant (autorisations vérifiées par un voter
dédié).
This commit is contained in:
2026-02-12 08:38:19 +01:00
parent e930c505df
commit 44ebe5e511
91 changed files with 10071 additions and 39 deletions

View File

@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Infrastructure\Persistence\InMemory;
use App\Administration\Domain\Exception\StudentGuardianNotFoundException;
use App\Administration\Domain\Model\StudentGuardian\RelationshipType;
use App\Administration\Domain\Model\StudentGuardian\StudentGuardian;
use App\Administration\Domain\Model\StudentGuardian\StudentGuardianId;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryStudentGuardianRepository;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class InMemoryStudentGuardianRepositoryTest extends TestCase
{
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
private const string STUDENT_ID = '550e8400-e29b-41d4-a716-446655440002';
private const string GUARDIAN_ID = '550e8400-e29b-41d4-a716-446655440003';
private const string GUARDIAN_2_ID = '550e8400-e29b-41d4-a716-446655440004';
private InMemoryStudentGuardianRepository $repository;
protected function setUp(): void
{
$this->repository = new InMemoryStudentGuardianRepository();
}
#[Test]
public function saveAndGetReturnsLink(): void
{
$link = $this->createLink();
$this->repository->save($link);
$found = $this->repository->get($link->id, TenantId::fromString(self::TENANT_ID));
self::assertTrue($found->id->equals($link->id));
}
#[Test]
public function getThrowsWhenNotFound(): void
{
$this->expectException(StudentGuardianNotFoundException::class);
$this->repository->get(StudentGuardianId::generate(), TenantId::fromString(self::TENANT_ID));
}
#[Test]
public function findGuardiansForStudentReturnsLinks(): void
{
$link1 = $this->createLink();
$link2 = $this->createLink(guardianId: self::GUARDIAN_2_ID, type: RelationshipType::MOTHER);
$this->repository->save($link1);
$this->repository->save($link2);
$guardians = $this->repository->findGuardiansForStudent(
UserId::fromString(self::STUDENT_ID),
TenantId::fromString(self::TENANT_ID),
);
self::assertCount(2, $guardians);
}
#[Test]
public function findStudentsForGuardianReturnsLinks(): void
{
$link = $this->createLink();
$this->repository->save($link);
$students = $this->repository->findStudentsForGuardian(
UserId::fromString(self::GUARDIAN_ID),
TenantId::fromString(self::TENANT_ID),
);
self::assertCount(1, $students);
self::assertTrue($students[0]->studentId->equals(UserId::fromString(self::STUDENT_ID)));
}
#[Test]
public function countGuardiansForStudentReturnsCorrectCount(): void
{
$studentId = UserId::fromString(self::STUDENT_ID);
$tenantId = TenantId::fromString(self::TENANT_ID);
self::assertSame(0, $this->repository->countGuardiansForStudent($studentId, $tenantId));
$this->repository->save($this->createLink());
self::assertSame(1, $this->repository->countGuardiansForStudent($studentId, $tenantId));
$this->repository->save($this->createLink(guardianId: self::GUARDIAN_2_ID, type: RelationshipType::MOTHER));
self::assertSame(2, $this->repository->countGuardiansForStudent($studentId, $tenantId));
}
#[Test]
public function findByStudentAndGuardianReturnsLink(): void
{
$link = $this->createLink();
$this->repository->save($link);
$found = $this->repository->findByStudentAndGuardian(
UserId::fromString(self::STUDENT_ID),
UserId::fromString(self::GUARDIAN_ID),
TenantId::fromString(self::TENANT_ID),
);
self::assertNotNull($found);
self::assertTrue($found->id->equals($link->id));
}
#[Test]
public function findByStudentAndGuardianReturnsNullWhenNotFound(): void
{
$found = $this->repository->findByStudentAndGuardian(
UserId::fromString(self::STUDENT_ID),
UserId::fromString(self::GUARDIAN_ID),
TenantId::fromString(self::TENANT_ID),
);
self::assertNull($found);
}
#[Test]
public function deleteRemovesLink(): void
{
$link = $this->createLink();
$this->repository->save($link);
$this->repository->delete($link->id, $link->tenantId);
self::assertSame(0, $this->repository->countGuardiansForStudent(
$link->studentId,
$link->tenantId,
));
}
private function createLink(
string $guardianId = self::GUARDIAN_ID,
RelationshipType $type = RelationshipType::FATHER,
): StudentGuardian {
return StudentGuardian::lier(
studentId: UserId::fromString(self::STUDENT_ID),
guardianId: UserId::fromString($guardianId),
relationshipType: $type,
tenantId: TenantId::fromString(self::TENANT_ID),
createdAt: new DateTimeImmutable('2026-02-10 10:00:00'),
);
}
}