Procházet zdrojové kódy

Merge branch 'v8-4766_registration_status' into release/2.4.beta

Olivier Massot před 2 roky
rodič
revize
452f7a4612
30 změnil soubory, kde provedl 1806 přidání a 63 odebrání
  1. 2 0
      config/opentalent/products.yaml
  2. 59 0
      src/ApiResources/OnlineRegistration/RegistrationAvailability.php
  3. 48 0
      src/ApiResources/OnlineRegistration/RegistrationStatus.php
  4. 26 0
      src/Entity/Access/Access.php
  5. 44 2
      src/Entity/AccessWish/AccessFamilyWish.php
  6. 52 1
      src/Entity/AccessWish/AccessWish.php
  7. 40 1
      src/Entity/AccessWish/EducationStudentWish.php
  8. 90 0
      src/Entity/OnlineRegistration/OnlineRegistrationOpeningPeriod.php
  9. 140 0
      src/Entity/OnlineRegistration/OnlineRegistrationSettings.php
  10. 14 1
      src/Entity/Organization/Organization.php
  11. 8 0
      src/Enum/Export/ExportFormatEnum.php
  12. 21 0
      src/Enum/OnlineRegistration/RegistrationStatusEnum.php
  13. 21 0
      src/Enum/OnlineRegistration/ValidationStateEnum.php
  14. 19 0
      src/Enum/OnlineRegistration/WishRegistrationEnum.php
  15. 45 0
      src/Service/Access/Utils.php
  16. 9 7
      src/Service/Export/Encoder/DocXEncoder.php
  17. 2 1
      src/Service/Network/Utils.php
  18. 34 0
      src/Service/OnlineRegistration/OnlineRegistrationService.php
  19. 286 0
      src/Service/OnlineRegistration/RegistrationStartingService.php.off
  20. 131 0
      src/Service/OnlineRegistration/RegistrationStatusService.php
  21. 6 4
      src/Service/Rest/ApiRequestService.php
  22. 32 0
      src/Service/Utils/ArrayUtils.php
  23. 16 3
      src/Service/Utils/DatesUtils.php
  24. 50 0
      src/State/Provider/OnlineRegistration/RegistrationAvailabilityProvider.php
  25. 66 0
      src/State/Provider/OnlineRegistration/RegistrationStatusProvider.php
  26. 130 20
      tests/Unit/Service/Access/UtilsTest.php
  27. 15 14
      tests/Unit/Service/Network/UtilsTest.php
  28. 333 0
      tests/Unit/Service/OnlineRegistration/RegistrationStatusServiceTest.php.off
  29. 43 0
      tests/Unit/Service/Utils/ArrayUtilsTest.php
  30. 24 9
      tests/Unit/Service/Utils/DatesUtilsTest.php

+ 2 - 0
config/opentalent/products.yaml

@@ -227,6 +227,8 @@ opentalent:
             - AccessTmp
             - EducationStudentWish
             - OnlineRegistrationSettings
+            - RegistrationStatus
+            - RegistrationAvailability
           roles:
             - ROLE_ONLINEREGISTRATION_ADMINISTRATION
 

+ 59 - 0
src/ApiResources/OnlineRegistration/RegistrationAvailability.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace App\ApiResources\OnlineRegistration;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use App\ApiResources\ApiResourcesInterface;
+use App\State\Provider\OnlineRegistration\RegistrationAvailabilityProvider;
+
+#[ApiResource(
+    operations: [
+        new Get(
+            uriTemplate: '/online_registration/availability/{accessId}',
+            requirements: ['accessId' => '\\d+'],
+            defaults: ['accessId' => 0],
+            provider: RegistrationAvailabilityProvider::class
+        )
+    ]
+)]
+class RegistrationAvailability implements ApiResourcesInterface
+{
+    #[ApiProperty(identifier: true)]
+    private int $accessId;
+
+    private bool $available;
+
+    private ?string $message = null;
+
+    public function getAccessId(): int
+    {
+        return $this->accessId;
+    }
+
+    public function setAccessId(int $accessId): void
+    {
+        $this->accessId = $accessId;
+    }
+
+    public function isAvailable(): bool
+    {
+        return $this->available;
+    }
+
+    public function setAvailable(bool $available): void
+    {
+        $this->available = $available;
+    }
+
+    public function getMessage(): ?string
+    {
+        return $this->message;
+    }
+
+    public function setMessage(?string $message): void
+    {
+        $this->message = $message;
+    }
+}

+ 48 - 0
src/ApiResources/OnlineRegistration/RegistrationStatus.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace App\ApiResources\OnlineRegistration;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use App\ApiResources\ApiResourcesInterface;
+use App\State\Provider\OnlineRegistration\RegistrationStatusProvider;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[ApiResource(
+    operations: [
+        new Get(
+            uriTemplate: '/online_registration/status/{accessId}',
+            requirements: ['accessId' => '\\d+'],
+            defaults: ['accessId' => 0],
+            provider: RegistrationStatusProvider::class
+        )
+    ]
+)]
+class RegistrationStatus implements ApiResourcesInterface
+{
+    #[ApiProperty(identifier: true)]
+    private int $accessId;
+
+    private ?string $status = null;
+
+    public function getAccessId(): int
+    {
+        return $this->accessId;
+    }
+
+    public function setAccessId(int $accessId): void
+    {
+        $this->accessId = $accessId;
+    }
+
+    public function getStatus(): ?string
+    {
+        return $this->status;
+    }
+
+    public function setStatus(?string $status): void
+    {
+        $this->status = $status;
+    }
+}

+ 26 - 0
src/Entity/Access/Access.php

@@ -195,6 +195,12 @@ class Access implements UserInterface, PasswordAuthenticatedUserInterface
     #[ORM\JoinColumn(referencedColumnName: 'id', nullable: false, onDelete: "SET NULL")]
     private AccessFamily $accessFamily;
 
+    #[ORM\Column]
+    private bool $newAccess = false;
+
+    #[ORM\Column]
+    private bool $ielEnabled = false;
+
     #[ORM\OneToMany(mappedBy: 'educationalProjectPayer', targetEntity: EducationalProjectPayer::class, cascade: ['persist'], orphanRemoval: true)]
     private Collection $billingEducationalProjectPayers;
 
@@ -1036,6 +1042,26 @@ class Access implements UserInterface, PasswordAuthenticatedUserInterface
         return $this;
     }
 
+    public function isNewAccess(): bool
+    {
+        return $this->newAccess;
+    }
+
+    public function setNewAccess(bool $newAccess): void
+    {
+        $this->newAccess = $newAccess;
+    }
+
+    public function isIelEnabled(): bool
+    {
+        return $this->ielEnabled;
+    }
+
+    public function setIelEnabled(bool $ielEnabled): void
+    {
+        $this->ielEnabled = $ielEnabled;
+    }
+
     /**
      * @return Collection<int, EducationalProjectPayer>
      */

+ 44 - 2
src/Entity/AccessWish/AccessFamilyWish.php

@@ -9,7 +9,7 @@ use Doctrine\ORM\Mapping as ORM;
 use Doctrine\Common\Collections\Collection;
 
 /**
- * Classe ... qui ...
+ * Famille à laquelle est rattaché un AccessWish
  */
 //#[Auditable]
 #[ORM\Entity]
@@ -23,6 +23,19 @@ class AccessFamilyWish
     #[ORM\OneToMany(mappedBy: 'accessFamilyWish', targetEntity: AccessWish::class, cascade: ['remove'])]
     private Collection $accessWishes;
 
+    /**
+     * Date de dernière mise à jour de l'entité
+     * @var \DateTimeInterface
+     */
+    #[ORM\Column(type: 'datetime', nullable: true)]
+    private \DateTimeInterface $updateDate;
+
+    #[ORM\Column]
+    private bool $registrationCompleted = false;
+
+    #[ORM\Column]
+    private bool $closeRegistration = false;
+
     public function __construct()
     {
         $this->accessWishes = new ArrayCollection();
@@ -63,4 +76,33 @@ class AccessFamilyWish
         return $this;
     }
 
-}
+    public function getUpdateDate(): \DateTimeInterface
+    {
+        return $this->updateDate;
+    }
+
+    public function setUpdateDate(\DateTimeInterface $updateDate): void
+    {
+        $this->updateDate = $updateDate;
+    }
+
+    public function isRegistrationCompleted(): bool
+    {
+        return $this->registrationCompleted;
+    }
+
+    public function setRegistrationCompleted(bool $registrationCompleted): void
+    {
+        $this->registrationCompleted = $registrationCompleted;
+    }
+
+    public function isCloseRegistration(): bool
+    {
+        return $this->closeRegistration;
+    }
+
+    public function setCloseRegistration(bool $closeRegistration): void
+    {
+        $this->closeRegistration = $closeRegistration;
+    }
+}

+ 52 - 1
src/Entity/AccessWish/AccessWish.php

@@ -10,12 +10,15 @@ use App\Entity\Core\File;
 use App\Entity\Core\Tagg;
 use App\Entity\Organization\Organization;
 //use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Auditable;
+use App\Enum\OnlineRegistration\ValidationStateEnum;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\ORM\Mapping as ORM;
 use Doctrine\Common\Collections\Collection;
+use \DateTime;
+use Symfony\Component\Validator\Constraints as Assert;
 
 /**
- * Classe ... qui ...
+ * Demande d'inscription d'un Access via l'IEL
  */
 //#[Auditable]
 #[ApiResource(operations: [])]
@@ -60,6 +63,24 @@ class AccessWish
     #[ORM\InverseJoinColumn(name: 'tag_id', referencedColumnName: 'id')]
     private Collection $tags;
 
+    #[ORM\Column(length: 50)]
+    #[Assert\Choice(callback: [ValidationStateEnum::class, 'toArray'])]
+    private string $validationState;
+
+    /**
+     * Date de création dde l'entité
+     * @var DateTime
+     */
+    #[ORM\Column(type: 'datetime', nullable: true)]
+    private \DateTimeInterface $createDate;
+
+    /**
+     * Date de dernière mise à jour de l'entité
+     * @var DateTime
+     */
+    #[ORM\Column(type: 'datetime', nullable: true)]
+    private \DateTimeInterface $updateDate;
+
     public function __construct()
     {
         $this->educationStudentWishes = new ArrayCollection();
@@ -246,4 +267,34 @@ class AccessWish
 
         return $this;
     }
+
+    public function getCreateDate(): DateTime
+    {
+        return $this->createDate;
+    }
+
+    public function setCreateDate(DateTime $createDate): void
+    {
+        $this->createDate = $createDate;
+    }
+
+    public function getUpdateDate(): DateTime
+    {
+        return $this->updateDate;
+    }
+
+    public function setUpdateDate(DateTime $updateDate): void
+    {
+        $this->updateDate = $updateDate;
+    }
+
+    public function getValidationState(): string
+    {
+        return $this->validationState;
+    }
+
+    public function setValidationState(string $validationState): void
+    {
+        $this->validationState = $validationState;
+    }
 }

+ 40 - 1
src/Entity/AccessWish/EducationStudentWish.php

@@ -8,7 +8,10 @@ use App\Entity\Education\EducationCurriculum;
 use App\Entity\Education\EducationStudent;
 use App\Entity\Education\EducationTiming;
 //use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Auditable;
+use App\Enum\OnlineRegistration\RegistrationStatusEnum;
+use App\Enum\OnlineRegistration\WishRegistrationEnum;
 use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Validator\Constraints as Assert;
 
 /**
  * Classe ... qui ...
@@ -22,6 +25,22 @@ class EducationStudentWish
     #[ORM\GeneratedValue]
     private ?int $id = null;
 
+    /**
+     * TODO: Documenter
+     * @var string|null
+     */
+    #[ORM\Column(length: 50)]
+    #[Assert\Choice(callback: [WishRegistrationEnum::class, 'toArray'])]
+    private ?string $wishRegistration = null;
+
+    /**
+     * Statut de l'enregistrement en ligne
+     * @var string|null
+     */
+    #[ORM\Column(length: 50)]
+    #[Assert\Choice(callback: [RegistrationStatusEnum::class, 'toArray'])]
+    private ?string $registrationStatus = null;
+
     #[ORM\ManyToOne]
     #[ORM\JoinColumn(referencedColumnName: 'id' ,nullable: true, onDelete: 'SET NULL')]
     private Education $educationWish;
@@ -49,6 +68,26 @@ class EducationStudentWish
         return $this->id;
     }
 
+    public function getWishRegistration(): ?string
+    {
+        return $this->wishRegistration;
+    }
+
+    public function setWishRegistration(?string $wishRegistration): void
+    {
+        $this->wishRegistration = $wishRegistration;
+    }
+
+    public function getRegistrationStatus(): ?string
+    {
+        return $this->registrationStatus;
+    }
+
+    public function setRegistrationStatus(?string $registrationStatus): void
+    {
+        $this->registrationStatus = $registrationStatus;
+    }
+
     public function getEducationWish(): ?Education
     {
         return $this->educationWish;
@@ -120,4 +159,4 @@ class EducationStudentWish
 
         return $this;
     }
-}
+}

+ 90 - 0
src/Entity/OnlineRegistration/OnlineRegistrationOpeningPeriod.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace App\Entity\OnlineRegistration;
+
+use ApiPlatform\Metadata\ApiResource;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * Période d'ouverture de l'IEL pour une organisation
+ */
+//#[ApiResource(operations: [])]
+//#[ORM\Entity]
+class OnlineRegistrationOpeningPeriod
+{
+    #[ORM\Id]
+    #[ORM\Column]
+    #[ORM\GeneratedValue]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'onlineRegistrationOpeningPeriods')]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?OnlineRegistrationSettings $onlineRegistrationSettings = null;
+
+    /**
+     * TODO: c'est quoi?
+     * @var OnlineRegistrationSettings|null
+     */
+    #[ORM\ManyToOne(inversedBy: 'onlineRegistrationOpeningPeriods')]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?OnlineRegistrationSettings $onlineRegistrationSettingsNewEnrolments = null;
+
+    /**
+     * Date de début de la période d'IEL
+     * @var \DateTimeInterface
+     */
+    #[ORM\Column(type: 'datetime', nullable: false)]
+    private \DateTimeInterface $startDate;
+
+    /**
+     * Date de fin de la période d'IEL
+     * @var \DateTimeInterface
+     */
+    #[ORM\Column(type: 'datetime', nullable: false)]
+    private \DateTimeInterface $endDate;
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getOnlineRegistrationSettings(): ?OnlineRegistrationSettings
+    {
+        return $this->onlineRegistrationSettings;
+    }
+
+    public function setOnlineRegistrationSettings(?OnlineRegistrationSettings $onlineRegistrationSettings): void
+    {
+        $this->onlineRegistrationSettings = $onlineRegistrationSettings;
+    }
+
+    public function getOnlineRegistrationSettingsNewEnrolments(): ?OnlineRegistrationSettings
+    {
+        return $this->onlineRegistrationSettingsNewEnrolments;
+    }
+
+    public function setOnlineRegistrationSettingsNewEnrolments(?OnlineRegistrationSettings $onlineRegistrationSettingsNewEnrolments): void
+    {
+        $this->onlineRegistrationSettingsNewEnrolments = $onlineRegistrationSettingsNewEnrolments;
+    }
+
+    public function getStartDate(): \DateTimeInterface
+    {
+        return $this->startDate;
+    }
+
+    public function setStartDate(\DateTimeInterface $startDate): void
+    {
+        $this->startDate = $startDate;
+    }
+
+    public function getEndDate(): \DateTimeInterface
+    {
+        return $this->endDate;
+    }
+
+    public function setEndDate(\DateTimeInterface $endDate): void
+    {
+        $this->endDate = $endDate;
+    }
+}

+ 140 - 0
src/Entity/OnlineRegistration/OnlineRegistrationSettings.php

@@ -0,0 +1,140 @@
+<?php
+
+namespace App\Entity\OnlineRegistration;
+
+use ApiPlatform\Metadata\ApiResource;
+use App\Entity\Organization\Organization;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ORM\Mapping as ORM;
+use JetBrains\PhpStorm\Pure;
+
+/**
+ * Configuration de l'IEL pour une organization
+ */
+//#[ApiResource(operations: [])]
+//#[ORM\Entity]
+class OnlineRegistrationSettings
+{
+    #[ORM\Id]
+    #[ORM\Column]
+    #[ORM\GeneratedValue]
+    private ?int $id = null;
+
+    #[ORM\OneToOne(inversedBy: 'organization', targetEntity: Organization::class)]
+    private Organization $organization;
+
+    #[ORM\OneToMany(mappedBy: 'onlineRegistrationSettings', targetEntity: OnlineRegistrationOpeningPeriod::class, orphanRemoval: true)]
+    private Collection $openingPeriods;
+
+    #[ORM\OneToMany(mappedBy: 'onlineRegistrationSettingsNewEnrolments', targetEntity: OnlineRegistrationOpeningPeriod::class, orphanRemoval: true)]
+    private Collection $openingPeriodsNewEnrolments;
+
+    /**
+     * TODO: compléter la doc
+     * @var bool
+     */
+    #[ORM\Column]
+    private bool $addNewStudents = false;
+
+    /**
+     * TODO: compléter la doc
+     * @var bool
+     */
+    #[ORM\Column]
+    private bool $addNewEducations = false;
+
+    #[Pure]
+    public function __construct()
+    {
+        $this->openingPeriods = new ArrayCollection();
+        $this->openingPeriodsNewEnrolments = new ArrayCollection();
+    }
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getOrganization(): Organization
+    {
+        return $this->organization;
+    }
+
+    public function setOrganization(Organization $organization): void
+    {
+        $this->organization = $organization;
+    }
+
+    public function getOnlineRegistrationOpeningPeriods(): Collection
+    {
+        return $this->openingPeriods;
+    }
+
+    public function addOnlineRegistrationOpeningPeriod(OnlineRegistrationOpeningPeriod $openingPeriod): self
+    {
+        if (!$this->openingPeriods->contains($openingPeriod)) {
+            $this->openingPeriods[] = $openingPeriod;
+            $openingPeriod->setOnlineRegistrationSettings($this);
+        }
+        return $this;
+    }
+
+    public function removeOnlineRegistrationOpeningPeriod(OnlineRegistrationOpeningPeriod $openingPeriod): self
+    {
+        if ($this->openingPeriods->removeElement($openingPeriod)) {
+            // set the owning side to null (unless already changed)
+            if ($openingPeriod->getOnlineRegistrationSettings() === $this) {
+                $openingPeriod->setOnlineRegistrationSettings(null);
+            }
+        }
+        return $this;
+    }
+
+    public function getOnlineRegistrationOpeningPeriodsNewEnrolments(): Collection
+    {
+        return $this->openingPeriodsNewEnrolments;
+    }
+
+    public function addOnlineRegistrationOpeningPeriodNewEnrolments(OnlineRegistrationOpeningPeriod $openingPeriod): self
+    {
+        if (!$this->openingPeriodsNewEnrolments->contains($openingPeriod)) {
+            $this->openingPeriodsNewEnrolments[] = $openingPeriod;
+            $openingPeriod->setOnlineRegistrationSettingsNewEnrolments($this);
+        }
+        return $this;
+    }
+
+    public function removeOnlineRegistrationOpeningPeriodNewEnrolments(OnlineRegistrationOpeningPeriod $openingPeriod): self
+    {
+        if ($this->openingPeriodsNewEnrolments->removeElement($openingPeriod)) {
+            // set the owning side to null (unless already changed)
+            if ($openingPeriod->getOnlineRegistrationSettingsNewEnrolments() === $this) {
+                $openingPeriod->setOnlineRegistrationSettingsNewEnrolments(null);
+            }
+        }
+        return $this;
+    }
+
+    // TODO: confirmer que l'action est bien `can`
+    public function canAddNewStudents(): bool
+    {
+        return $this->addNewStudents;
+    }
+
+    public function setAddNewStudents(bool $addNewStudents): void
+    {
+        $this->addNewStudents = $addNewStudents;
+    }
+
+    // TODO: confirmer que l'action est bien `can`
+    public function canAddNewEducations(): bool
+    {
+        return $this->addNewEducations;
+    }
+
+    public function setAddNewEducations(bool $addNewEducations): void
+    {
+        $this->addNewEducations = $addNewEducations;
+    }
+}

+ 14 - 1
src/Entity/Organization/Organization.php

@@ -38,7 +38,7 @@ use App\Entity\Product\Equipment;
 use App\Entity\Product\Intangible;
 use App\Repository\Organization\OrganizationRepository;
 use App\State\Processor\Organization\OrganizationProcessor;
-
+use \App\Entity\Organization\OnlineRegistrationSettings;
 //use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Auditable;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
@@ -314,6 +314,9 @@ class Organization
     #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Donor::class, orphanRemoval: true)]
     private Collection $donors;
 
+//    #[ORM\OneToOne()]
+//    private OnlineRegistrationSettings $onlineRegistrationSettings;
+
     #[ORM\ManyToMany(targetEntity: Tagg::class, inversedBy: 'organizations', cascade: ['persist'])]
     #[ORM\JoinTable(name: 'tag_organization')]
     #[ORM\JoinColumn(name: 'organization_id', referencedColumnName: 'id')]
@@ -1693,6 +1696,16 @@ class Organization
         return $this;
     }
 
+//    public function getOnlineRegistrationSettings(): \App\Entity\Organization\OnlineRegistrationSettings
+//    {
+//        return $this->onlineRegistrationSettings;
+//    }
+//
+//    public function setOnlineRegistrationSettings(\App\Entity\Organization\OnlineRegistrationSettings $onlineRegistrationSettings): void
+//    {
+//        $this->onlineRegistrationSettings = $onlineRegistrationSettings;
+//    }
+
     /**
      * @return Collection<int, Tagg>
      */

+ 8 - 0
src/Enum/Export/ExportFormatEnum.php

@@ -7,6 +7,13 @@ use MyCLabs\Enum\Enum;
 
 /**
  * Formats de sortie des fichiers exportés
+ *
+ * @method static PDF()
+ * @method static CSV()
+ * @method static TXT()
+ * @method static XLSX()
+ * @method static XML()
+ * @method static DOCX()
  */
 class ExportFormatEnum extends Enum
 {
@@ -15,4 +22,5 @@ class ExportFormatEnum extends Enum
     private const TXT = 'txt';
     private const XLSX = 'xlsx';
     private const XML = 'xml';
+    private const DOCX = 'docx';
 }

+ 21 - 0
src/Enum/OnlineRegistration/RegistrationStatusEnum.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Enum\OnlineRegistration;
+
+use MyCLabs\Enum\Enum;
+
+/**
+ * Statut de l'enregistrement en ligne
+ *
+ * @method static NEGOTIABLE()
+ * @method static ACCEPTED()
+ * @method static PENDING()
+ * @method static DENIED()
+ */
+class RegistrationStatusEnum extends Enum
+{
+    private const NEGOTIABLE = 'NEGOTIABLE';
+    private const ACCEPTED = 'ACCEPTED';
+    private const PENDING = 'PENDING';
+    private const DENIED = 'DENIED';
+}

+ 21 - 0
src/Enum/OnlineRegistration/ValidationStateEnum.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Enum\OnlineRegistration;
+
+use MyCLabs\Enum\Enum;
+
+/**
+ * Niveau de validation d'une inscription en ligne
+ *
+ * TODO: à valider
+ *
+ * @method static OPEN()
+ * @method static OPEN_FOR_EDUCATIONS()
+ * @method static CLOSE()
+ */
+class ValidationStateEnum extends Enum
+{
+    const OPEN = 'OPEN';
+    const OPEN_FOR_EDUCATIONS = 'OPEN_FOR_EDUCATIONS';
+    const CLOSE = 'CLOSE';
+}

+ 19 - 0
src/Enum/OnlineRegistration/WishRegistrationEnum.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Enum\OnlineRegistration;
+
+use MyCLabs\Enum\Enum;
+
+/**
+ * TODO: documenter
+ *
+ * @method static NO_INFORMATION()
+ * @method static REREGISTER()
+ * @method static STOP_REGISTRATION()
+ */
+class WishRegistrationEnum extends Enum
+{
+    const NO_INFORMATION = 'NO_INFORMATION';
+    const REREGISTER = 'REREGISTER';
+    const STOP_REGISTRATION = 'STOP_REGISTRATION';
+}

+ 45 - 0
src/Service/Access/Utils.php

@@ -7,6 +7,7 @@ use App\Entity\Access\Access;
 use App\Entity\Organization\Organization;
 use App\Repository\Access\AccessRepository;
 use App\Service\ServiceIterator\OptionalsRolesIterator;
+use App\Service\Utils\DatesUtils;
 use App\Test\Service\Access\UtilsTest;
 use Doctrine\Common\Collections\Criteria;
 use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
@@ -79,4 +80,48 @@ class Utils
             'organization' => $organization
         ]);
     }
+
+    /**
+     * Retourne les noms des fonctions actives d'un access au sein d'une organisation
+     *
+     * @param Access $access
+     * @return array<string>
+     * @throws \Exception
+     */
+    public function getActiveFunctions(Access $access): array {
+        $functions = [];
+
+        foreach ($access->getOrganizationFunction() as $function) {
+            if (
+                $function->getStartDate() !== null &&
+                DatesUtils::new() <= $function->getStartDate()->sub(new \DateInterval('PT23H59M'))
+            ) {
+                // La fonction n'est pas encore active
+                continue;
+            }
+            if (
+                $function->getEndDate() !== null &&
+                DatesUtils::new() >= $function->getEndDate()->add(new \DateInterval('PT23H59M'))
+            ) {
+                // La fonction n'est plus active
+                continue;
+            }
+
+            $functions[] = $function->getFunctionType()->getMission();
+        }
+
+        return $functions;
+    }
+
+    /**
+     * L'access possède-t-il la fonction donnée au sein de son organisation
+     *
+     * @param Access $access
+     * @param string $functionName
+     * @return bool
+     * @throws \Exception
+     */
+    public function hasActiveFunction(Access $access, string $functionName): bool {
+        return in_array($functionName, $this->getActiveFunctions($access));
+    }
 }

+ 9 - 7
src/Service/Export/Encoder/DocXEncoder.php

@@ -28,12 +28,14 @@ class DocXEncoder implements EncoderInterface
      */
     public function encode(string $html, array $options = []): string
     {
-        $docx = new CreateDocx();
-        $docx->embedHTML($html);
-        $tempFile = tempnam(sys_get_temp_dir(), 'docx');
-        $docx->createDocx($tempFile);
-        $content = file_get_contents($tempFile);
-        unlink($tempFile);
-        return $content;
+//        $docx = new CreateDocx();
+//        $docx->embedHTML($html);
+//        $tempFile = tempnam(sys_get_temp_dir(), 'docx');
+//        $docx->createDocx($tempFile);
+//        $content = file_get_contents($tempFile);
+//        unlink($tempFile);
+//        return $content;
+
+        return '';
     }
 }

+ 2 - 1
src/Service/Network/Utils.php

@@ -66,6 +66,7 @@ class Utils
      */
     public function isNetworkOrganizationActiveNow(NetworkOrganization $networksOrganization): bool
     {
-        return $networksOrganization->getStartDate() && DatesUtils::isIntervalIsValidNow($networksOrganization->getStartDate(), $networksOrganization->getEndDate());
+        return $networksOrganization->getStartDate() &&
+            DatesUtils::isNowInInterval($networksOrganization->getStartDate(), $networksOrganization->getEndDate());
     }
 }

+ 34 - 0
src/Service/OnlineRegistration/OnlineRegistrationService.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Service\OnlineRegistration;
+
+use App\Entity\Access\Access;
+use App\Entity\AccessWish\AccessWish;
+use App\Service\Utils\DatesUtils;
+
+/**
+ * --- SERVICE INUTILISE POUR LE MOMENT ---
+ */
+class OnlineRegistrationService
+{
+
+    /**
+     * Retourne le premier AccessWish correspondant à l'année en cours, ou null si aucun n'est trouvé
+     *
+     * @param Access $access
+     * @return AccessWish | null
+     */
+    public function getCurrentAccessWish(Access $access): AccessWish | null {
+        $currentYear =  DatesUtils::new()->format('Y');
+
+        foreach ($access->getAccessWishes() as $accessWish)
+        {
+            /** @var AccessWish $accessWish */
+            if ($accessWish->getCreateDate()->format('Y') === $currentYear) {
+                return $accessWish;
+            }
+        }
+
+        return null;
+    }
+}

+ 286 - 0
src/Service/OnlineRegistration/RegistrationStartingService.php.off

@@ -0,0 +1,286 @@
+<?php
+
+namespace App\Service\OnlineRegistration;
+
+use App\Entity\Access\Access;
+use App\Entity\Organization\Organization;
+use App\Enum\OnlineRegistration\ValidationStateEnum;
+use App\Service\Access\Utils as AccessUtils;
+use App\Service\Utils\ArrayUtils;
+use App\Service\Utils\DatesUtils;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+
+
+/**
+ * --- SERVICE INUTILISE POUR LE MOMENT ---
+ * Service de lancement de l'inscription en ligne
+ */
+class RegistrationStartingService
+{
+    const EDUCATION_REREGISTER_RIGHT = "canReregisterEducations";
+    const EDUCATION_ADD_RIGHT = "canAddEducations";
+    const STUDENT_ADD_RIGHT = "canAddStudents";
+    const ONLY_EDUCATION_ADD_RIGHT = "lockAllExeptAddNewEducation";
+
+
+    public function __construct(
+        private OnlineRegistrationService $onlineRegistrationService,
+        private AccessUtils $accessUtils
+    ) {}
+
+    /**
+     * Teste si un Access peut accéder à l'IEL
+     * Sinon, lève une RuntimeException
+     *
+     * @param Access $access
+     * @return bool
+     */
+    public function canStartOnlineRegistration(Access $access): bool {
+
+        if ($this->accessAndFamilyWishAlreadyValidateCanAddEducation($access)) {
+            return true;
+        }
+
+        $functions = $this->accessUtils->getActiveFunctions($access);
+
+        $hasNoFunction = $functions === ["OTHER_FUNCTION"] && !$access->getAdminAccess();
+
+        $isStudent = in_array('STUDENT', $functions);
+
+        if (!$isStudent && $access->getAccessFamily()) {
+            // On regarde si un membre de la famille est un élève actif
+            $isStudent = ArrayUtils::any(
+                $access->getAccessFamily()->getAccesses()->toArray(),
+                function ($access) { return $this->accessUtils->hasActiveFunction($access, 'STUDENT'); }
+            );
+        }
+
+        $reregistration = $isStudent && !$access->isNewAccess();
+
+        // La structure dois posséder des periodes d'ouvertures
+        if (!$this->hasOpeningHours($access->getOrganization(), $reregistration)) {
+            throw new \RuntimeException('no_opening_hours_users');
+        }
+
+        // Aujourd'hui doit faire partie des périodes d'ouvertures
+        if(!$this->isInOpeningPeriod($access->getOrganization(), $reregistration)) {
+            throw new \RuntimeException('not_inside_period');
+        }
+
+        // Si l'Access a déjà répondu une fois cette année à l'IEL (ou un membre de la famille)
+        if($this->checkIelValidationDateTimeFamily($access)) {
+            throw new \RuntimeException('iel_allready_done');
+        }
+        else if (!$hasNoFunction && !$access->isIelEnabled()) {
+            throw new \RuntimeException('no_access_granted');
+        }
+
+        return true;
+    }
+
+    /**
+     * TODO: compléter la doc et trouver un nom plus court
+     *
+     * @param Access $access
+     * @return bool
+     * @throws \Exception
+     */
+    private function accessAndFamilyWishAlreadyValidateCanAddEducation(Access $access): bool {
+        $currentAccessWish = $this->onlineRegistrationService->getCurrentAccessWish($access);
+
+        if (!$currentAccessWish) {
+            return false;
+        }
+
+        $accessWishAlreadyValidateCanAddEducation =
+            $this->accessWishAlreadyValidateCanAddEducation($access) ||
+            (
+                $access->getAccessFamily() &&
+                !$access->getAccessFamily()->getAccesses()->isEmpty() &&
+                ArrayUtils::any(
+                    $access->getAccessFamily()->getAccesses()->toArray(),
+                    function ($familyAccess) { return $this->accessWishAlreadyValidateCanAddEducation($familyAccess); }
+                )
+            );
+
+        if ($accessWishAlreadyValidateCanAddEducation) {
+            // La structure dois posséder des periodes d'ouvertures
+            if (!$this->hasOpeningHours($access->getOrganization(),false)){
+                throw new \Exception('no_opening_hours_users');
+            }
+
+            // Aujourd'hui doit faire partit des périodes d'ouvertures
+            if (!$this->isInsidePeriod($access->getOrganization(),false)){
+                throw new \Exception('not_inside_period');
+            }
+        }
+
+        return $accessWishAlreadyValidateCanAddEducation;
+    }
+
+
+    /**
+     * TODO: compléter la doc et trouver un nom plus court
+     *
+     * @param Access $access
+     * @return bool
+     */
+    private function accessWishAlreadyValidateCanAddEducation(Access $access): bool
+    {
+        $currentAccessWish = $this->onlineRegistrationService->getCurrentAccessWish($access);
+
+        if (!$currentAccessWish) {
+            return false;
+        }
+
+        $accessFamilyWish = $currentAccessWish->getAccessFamilyWish();
+
+        if ($accessFamilyWish?->isCloseRegistration()) {
+            return false;
+        }
+
+        if ($currentAccessWish->getValidationState() !== ValidationStateEnum::OPEN_FOR_EDUCATIONS) {
+            return false;
+        }
+
+        $studentsAndEducationsAuthorization = $this->buildStudentAuthorizations($access);
+
+        return $studentsAndEducationsAuthorization[self::EDUCATION_ADD_RIGHT] === true;
+    }
+
+    /**
+     * Détermine les autorisations d'un étudiant
+     *
+     * @param Access $access
+     * @return array<string, bool>
+     */
+    private function buildStudentAuthorizations(Access $access): array {
+
+        $rights = [
+            self::EDUCATION_REREGISTER_RIGHT => false,
+            self::EDUCATION_ADD_RIGHT => true,
+            self::STUDENT_ADD_RIGHT => true,
+            self::ONLY_EDUCATION_ADD_RIGHT => false
+        ];
+
+        $isNewStudent = $access->isNewAccess();
+        if ($isNewStudent) {
+            return $rights;
+        }
+
+        $organization = $access->getOrganization();
+        if ($organization === null) {
+            throw new \RuntimeException('Missing organization');
+        }
+
+        $organizationSettings = $organization->getOnlineRegistrationSettings();
+        $addNewStudentsSetting = $organizationSettings->canAddNewStudents();
+        $addNewEducationsSetting = $organizationSettings->canAddNewEducations();
+
+        $reRegistrationIsOpen = $this->isRegistrationOpen($organization, true);
+        // TODO: je comprend pas bien cette condition?
+        $rights[self::EDUCATION_REREGISTER_RIGHT] = $reRegistrationIsOpen || (!$addNewStudentsSetting && !$addNewEducationsSetting);
+
+        $newRegistrationIsOpen = $this->isRegistrationOpen($organization, false);
+        $rights[self::EDUCATION_ADD_RIGHT] =  $newRegistrationIsOpen || $addNewEducationsSetting;
+        $rights[self::STUDENT_ADD_RIGHT] =  $newRegistrationIsOpen || $addNewStudentsSetting;
+
+        $currentAccessWish = $this->onlineRegistrationService->getCurrentAccessWish($access);
+
+        $rights[self::STUDENT_ADD_RIGHT] = $currentAccessWish &&
+            $currentAccessWish->getValidationState() === ValidationStateEnum::OPEN_FOR_EDUCATIONS ||
+            (
+                $currentAccessWish->getAccessFamilyWish() &&
+                ArrayUtils::any(
+                    $currentAccessWish->getAccessFamilyWish()->getAccessWishes()->toArray(),
+                    function ($wish) { return $wish->getValidationState() === ValidationStateEnum::OPEN_FOR_EDUCATIONS; }
+                )
+            );
+
+        return $rights;
+    }
+
+    /**
+     * Retourne true si l'IEL est ouvert pour l'organisation donnée
+     *
+     * @param Organization $organization
+     * @param bool $reregistration
+     * @return bool
+     */
+    private function isRegistrationOpen(Organization $organization, bool $reregistration = true) {
+        return $this->hasOpeningHours($organization, $reregistration) &&
+               $this->isInOpeningPeriod($organization, $reregistration);
+    }
+
+    /**
+     * Retourne les périodes d'ouverture de l'IEL d'une organization, selon qu'il s'agisse de réinscription ou non
+     *
+     * @param Organization $organization
+     * @param bool $reregistration
+     * @return Collection
+     */
+    private function getOrganizationPeriods(Organization $organization, bool $reregistration = true): Collection {
+        $organizationSettings = $organization->getOnlineRegistrationSettings();
+        return $reregistration ?
+            $organizationSettings->getOpeningPeriods() :
+            $organizationSettings->getOpeningPeriodsNewEnrolments();
+    }
+
+    /**
+     * TODO: completer doc
+     *
+     * @param Organization $organization
+     * @param bool $reregistration
+     * @return bool
+     */
+    private function hasOpeningHours(Organization $organization, bool $reregistration = true): bool {
+        $openingPeriods = $this->getOrganizationPeriods($organization, $reregistration);
+        return count($openingPeriods) > 0;
+    }
+
+    /**
+     * Vérifie que nous sommes actuellement dans au moins une des périodes d'ouverture de l'IEL de l'organisation
+     *
+     * @param Organization $organization
+     * @param $reregistration
+     * @return bool
+     */
+    public function isInOpeningPeriod(Organization $organization, bool $reregistration = true): bool {
+        return ArrayUtils::any(
+            $this->getOrganizationPeriods($organization, $reregistration)->toArray(),
+            function ($period) { return DatesUtils::isNowInInterval($period->getStartDate(), $period->getEndDate()); }
+        );
+    }
+
+    /**
+     * TODO: completer doc et revoir le nom
+     *
+     * @param Access $access
+     * @return bool
+     */
+    private function checkIelValidationDateTimeFamily(Access $access): bool {
+        $today = DatesUtils::new();
+
+        if(
+            $access->getIelValidationDatetime() &&
+            ($access->getIelValidationDatetime()->format('Y') === $today->format('Y'))
+        ) {
+            return true;
+        }
+
+        if (
+            $access->getAccessFamily() &&
+            !$access->getAccessFamily()->getAccesses()->isEmpty()
+        ) {
+            return ArrayUtils::any(
+                $access->getAccessFamily()->getAccesses()->toArray(),
+                function ($access) use ($today) {
+                    return $access->getIelValidationDatetime() && $access->getIelValidationDatetime()->format('Y') === $today->format('Y');
+                }
+            );
+        }
+
+        return false;
+    }
+}

+ 131 - 0
src/Service/OnlineRegistration/RegistrationStatusService.php

@@ -0,0 +1,131 @@
+<?php
+
+namespace App\Service\OnlineRegistration;
+
+use App\Entity\AccessWish\AccessWish;
+use App\Entity\Access\Access;
+use App\Service\Utils\DatesUtils;
+use App\Enum\OnlineRegistration\RegistrationStatusEnum;
+use App\Enum\OnlineRegistration\WishRegistrationEnum;
+
+/**
+ * --- SERVICE INUTILISE POUR LE MOMENT ---
+ * Fournit le statut de l'enregistrement en ligne d'un Access
+ */
+class RegistrationStatusService
+{
+    /**
+     * Temps de validité du statut (en jours)
+     * (correspond au temps d'affichage de l'avertissement dans l'application)
+     */
+    const DISPLAYING_TIME = 30;
+
+    public function __construct(
+        private OnlineRegistrationService $onlineRegistrationService
+    ) {}
+
+    /**
+     * Détermine er retourne le statut de l'enregistrement en ligne d'un Access.
+     *
+     * @param Access $access
+     * @return string|null
+     */
+    public function getStatus(Access $access): string | null {
+
+        $currentAccessWish = $this->onlineRegistrationService->getCurrentAccessWish($access);
+        if ($currentAccessWish === null) {
+            // TODO: est-ce qu'il ne faudrait pas plutôt lever une erreur ici plutôt que de retourner null?
+            return null;
+        }
+
+        if (!$currentAccessWish->getAccessFamilyWish()->isRegistrationCompleted()) {
+            // TODO: est-ce qu'on ne retournerait pas un statut incomplet plutôt que null?
+            return null;
+        }
+
+        $wishesCountByStatus = $this->countEducationsByRegistrationStatus($currentAccessWish);
+        if ($wishesCountByStatus === null) {
+            // TODO: est-ce qu'il ne faudrait pas plutôt lever une erreur ici plutôt que de retourner null?
+            return null;
+        }
+
+        $hasNegotiable = $wishesCountByStatus[RegistrationStatusEnum::NEGOTIABLE()->getValue()] > 0;
+        $hasAccepted = $wishesCountByStatus[RegistrationStatusEnum::ACCEPTED()->getValue()] > 0;
+        $hasPending = $wishesCountByStatus[RegistrationStatusEnum::PENDING()->getValue()] > 0;
+        $hasDenied = $wishesCountByStatus[RegistrationStatusEnum::DENIED()->getValue()] > 0;
+
+        // Après acceptation ou refus, le bandeau est affiché pour une durée de 30 jours.
+        if(
+            !$hasNegotiable && ($hasAccepted || $hasDenied) &&
+            $this->getDaysSinceLastUpdate($currentAccessWish) > self::DISPLAYING_TIME
+        ) {
+            // TODO: est-ce qu'on ne retournerait pas ici un statut EXPIRED par ex?
+            return null;
+        }
+
+        if (!$hasAccepted && !$hasPending && $hasNegotiable) {
+            // La demande est dans la liste des inscriptions en ligne et n'a pas été traitée par l'administration
+            return RegistrationStatusEnum::NEGOTIABLE()->getValue();
+        }
+        else if ($hasPending && !$hasAccepted) {
+            // La demande est dans la liste des inscriptions en ligne car l'administration a mis "En attente" à au moins l'un des enseignements.
+            return RegistrationStatusEnum::PENDING()->getValue();
+        }
+        else if ($hasAccepted)
+        {
+            // La demande a été traitée par l'administration. L'élève a été accepté pour au moins l'une de ses activités.
+            // Et il a été placé dans la liste des "Inscriptions rentrée prochaine".
+            return RegistrationStatusEnum::ACCEPTED()->getValue();
+        }
+        else if ($hasDenied && !$hasNegotiable) {
+            // La demande a été traitée par l'administration. L'élève n'a pas été accepté dans l'établissement.
+            // Il est dans la liste des "Inscriptions en ligne" avec le statut "Refusé" sur tous ses enseignements.
+            return RegistrationStatusEnum::DENIED();
+        }
+
+        // TODO: est-ce qu'il ne faudrait pas plutôt lever une erreur ici plutôt que de retourner null?
+        return null;
+    }
+
+    public function getDaysSinceLastUpdate(AccessWish $accessWish): int {
+        $lastUpdate = $accessWish->getAccessFamilyWish()->getUpdateDate();
+        return DatesUtils::daysSince($lastUpdate);
+    }
+
+    /**
+     * Retourne le décompte des souhaits de l'utilisateur par RegistrationStatus, ou null si aucun souhaits.
+     *
+     * @param AccessWish $currentAccessWish
+     * @return array<string, int> | null
+     */
+    protected function countEducationsByRegistrationStatus(AccessWish $currentAccessWish): array | null {
+
+        $wishes = $currentAccessWish->getEducationStudentWishes();
+        $reregistrationWishes = $currentAccessWish->getEducationStudentReregistrationsWishes();
+
+        if ($reregistrationWishes->isEmpty() && $wishes->isEmpty()) {
+            // TODO: est-ce qu'il ne faudrait pas plutôt lever une erreur ici plutôt que de retourner null?
+            return null;
+        }
+
+        $registrationStatuses = [
+            RegistrationStatusEnum::NEGOTIABLE()->getValue() => 0,
+            RegistrationStatusEnum::ACCEPTED()->getValue() => 0,
+            RegistrationStatusEnum::PENDING()->getValue() => 0,
+            RegistrationStatusEnum::DENIED()->getValue() => 0
+        ];
+
+        foreach ($reregistrationWishes as $reregistrationWish) {
+            if ($reregistrationWish->getWishRegistration() !== WishRegistrationEnum::REREGISTER()->getValue()) {
+                continue;
+            }
+            $registrationStatuses[$reregistrationWish->getRegistrationStatus()]++;
+        }
+
+        foreach ($wishes as $educationStudentWish){
+            $registrationStatuses[$educationStudentWish->getRegistrationStatus()]++;
+        }
+
+        return $registrationStatuses;
+    }
+}

+ 6 - 4
src/Service/Rest/ApiRequestService.php

@@ -27,13 +27,14 @@ class ApiRequestService implements ApiRequestInterface
      * @param string $path
      * @param array<mixed> $parameters
      * @param array<mixed> $options
+     * @param boolean $throw @see https://symfony.com/doc/current/http_client.html#handling-exceptions
      * @return array<mixed>
      * @throws HttpException
      * @throws \JsonException
      */
-    public function getJsonContent(string $path, array $parameters = [], array $options = []): array
+    public function getJsonContent(string $path, array $parameters = [], array $options = [], bool $throw = true): array
     {
-        return json_decode($this->getContent($path, $parameters, $options), true, 512, JSON_THROW_ON_ERROR);
+        return json_decode($this->getContent($path, $parameters, $options, $throw), true, 512, JSON_THROW_ON_ERROR);
     }
 
     /**
@@ -42,13 +43,14 @@ class ApiRequestService implements ApiRequestInterface
      * @param string $path
      * @param array<mixed> $parameters
      * @param array<mixed> $options
+     * @param boolean $throw   @see https://symfony.com/doc/current/http_client.html#handling-exceptions
      * @return string
      * @throws HttpException
      */
-    public function getContent(string $path, array $parameters = [], array $options = []): string
+    public function getContent(string $path, array $parameters = [], array $options = [], bool $throw = true): string
     {
         try {
-            return $this->get($path, $parameters, $options)->getContent();
+            return $this->get($path, $parameters, $options)->getContent($throw);
         } catch (ClientExceptionInterface | TransportExceptionInterface | RedirectionExceptionInterface | ServerExceptionInterface $e) {
             throw new HttpException($e->getCode(), 'Request error : ' . $e->getMessage(), $e);
         }

+ 32 - 0
src/Service/Utils/ArrayUtils.php

@@ -5,6 +5,38 @@ namespace App\Service\Utils;
 
 class ArrayUtils
 {
+    /**
+     * Vérifie que tous les éléments d'une array valident le callback
+     *
+     * @param array<mixed> $array
+     * @param callable $callback
+     * @return bool
+     */
+    public static function all(array $array, callable $callback): bool {
+        foreach ($array as $item){
+            if (!call_user_func($callback, $item)){
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Vérifie qu'au moins un élément d'une array valide le callback
+     *
+     * @param array<mixed> $array
+     * @param callable $callback
+     * @return bool
+     */
+    public static function any(array $array, callable $callback): bool {
+        foreach ($array as $item){
+            if (call_user_func($callback, $item)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Returns an array containing changes between the first array the keys/values from an array
      * which are absent or different from $initialData

+ 16 - 3
src/Service/Utils/DatesUtils.php

@@ -50,11 +50,24 @@ class DatesUtils
      * @param DateTimeInterface $dateStart
      * @param DateTimeInterface|null $dateEnd
      * @return bool
-     * @see DatesUtilsTest::testIsIntervalIsValidNow()
+     * @see DatesUtilsTest::testIsNowInInterval()
      */
-    public static function isIntervalIsValidNow(DateTimeInterface $dateStart, DateTimeInterface $dateEnd = null): bool
+    public static function isNowInInterval(DateTimeInterface $dateStart, DateTimeInterface $dateEnd = null): bool
     {
-        $now = new DateTime('now');
+        $now = self::new();
         return $dateStart <= $now && (is_null($dateEnd) || $dateEnd >= $now);
     }
+
+    /**
+     * Retourne le nombre de jours écoulés depuis la date passée en paramètre
+     *
+     * @param DateTimeInterface $dateTime
+     * @return int
+     * @throws Exception
+     */
+    public static function daysSince(DateTimeInterface $dateTime): int {
+        return (int)self::new()
+            ->diff($dateTime)
+            ->format("%a");
+    }
 }

+ 50 - 0
src/State/Provider/OnlineRegistration/RegistrationAvailabilityProvider.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace App\State\Provider\OnlineRegistration;
+
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProviderInterface;
+use App\ApiResources\OnlineRegistration\RegistrationAvailability;
+use App\Entity\Access\Access;
+use App\Service\ApiLegacy\ApiLegacyRequestService;
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+
+class RegistrationAvailabilityProvider implements ProviderInterface
+{
+    public function __construct(
+        private Security $security,
+        private ApiLegacyRequestService $apiLegacyRequestService
+    ) {}
+
+    /**
+     * @param Operation $operation
+     * @param mixed[] $uriVariables
+     * @param mixed[] $context
+     * @return RegistrationAvailability|null
+     */
+    public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?RegistrationAvailability
+    {
+        if($operation instanceof GetCollection) {
+            throw new \RuntimeException('not supported', 500);
+        }
+
+        /** @var Access $currentAccess */
+        $currentAccess = $this->security->getUser();
+        if ($uriVariables['accessId'] !== $currentAccess->getId()) {
+            throw new \RuntimeException('forbidden', 503);
+        }
+
+        $response = $this->apiLegacyRequestService->get("/api/online_registration/access_verification");
+
+        $content = json_decode($response->getContent(false), true, 512, JSON_THROW_ON_ERROR);
+
+        $availability = new RegistrationAvailability();
+        $availability->setAccessId($currentAccess->getId());
+        $availability->setAvailable($response->getStatusCode() === 200);
+        $availability->setMessage($content['message']);
+
+        return $availability;
+    }
+}

+ 66 - 0
src/State/Provider/OnlineRegistration/RegistrationStatusProvider.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace App\State\Provider\OnlineRegistration;
+
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProviderInterface;
+use App\ApiResources\OnlineRegistration\RegistrationStatus;
+use App\Entity\Access\Access;
+use App\Enum\OnlineRegistration\RegistrationStatusEnum;
+use App\Repository\Access\AccessRepository;
+use App\Service\ApiLegacy\ApiLegacyRequestService;
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
+use RuntimeException;
+
+class RegistrationStatusProvider implements ProviderInterface
+{
+    public function __construct(
+        private Security $security,
+        private ApiLegacyRequestService $apiLegacyRequestService
+    ) {}
+
+    /**
+     * @param Operation $operation
+     * @param mixed[] $uriVariables
+     * @param mixed[] $context
+     * @return RegistrationStatus|null
+     */
+    public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?RegistrationStatus {
+        if($operation instanceof GetCollection) {
+            throw new RuntimeException('not supported', 500);
+        }
+
+        /** @var Access $currentAccess */
+        $currentAccess = $this->security->getUser();
+        if ($uriVariables['accessId'] !== $currentAccess->getId()) {
+            throw new RuntimeException('forbidden', 503);
+        }
+
+        // --- L'appel au service remplacera l'appel à l'API v1 à l'avenir --
+        // $registrationStatusValue = $this->registrationStatusService->getStatus($currentAccess);
+
+        $response = $this->apiLegacyRequestService->getJsonContent("/api/online_registration/registration_status");
+        if (!isset($response['status'])) {
+            throw new RuntimeException('An error occured', 500);
+        }
+
+        $statusMap = [
+            "your_application_is_awaiting_processing" => RegistrationStatusEnum::NEGOTIABLE()->getValue(),
+            "your_registration_file_has_been_validated" => RegistrationStatusEnum::ACCEPTED()->getValue(),
+            "you_have_been_placed_on_the_waiting_list" => RegistrationStatusEnum::PENDING()->getValue(),
+            "your_application_has_been_refused" => RegistrationStatusEnum::DENIED()->getValue(),
+            null => null
+        ];
+
+        $status  = $statusMap[$response['status']];
+
+        $registrationStatus = new RegistrationStatus();
+        $registrationStatus->setAccessId($currentAccess->getId());
+        $registrationStatus->setStatus($status);
+
+        return $registrationStatus;
+    }
+
+}

+ 130 - 20
tests/Unit/Service/Access/UtilsTest.php

@@ -3,10 +3,15 @@
 namespace App\Tests\Unit\Service\Access;
 
 use App\Entity\Access\Access;
+use App\Entity\Access\FunctionType;
+use App\Entity\Access\OrganizationFunction;
 use App\Entity\Organization\Organization;
 use App\Repository\Access\AccessRepository;
 use App\Service\Access\Utils as AccessUtils;
 use App\Service\ServiceIterator\OptionalsRolesIterator;
+use App\Service\Utils\DatesUtils;
+use Doctrine\Common\Collections\ArrayCollection;
+use Hoa\Iterator\Mock;
 use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Symfony\Component\Security\Core\Role\RoleHierarchy;
@@ -24,15 +29,25 @@ class UtilsTest extends TestCase
         $this->accessRepository = $this->getMockBuilder(AccessRepository::class)->disableOriginalConstructor()->getMock();
     }
 
+    public function tearDown(): void
+    {
+        DatesUtils::clearFakeDatetime();
+    }
+
+    private function buildAccessUtilsMockForMethod(string $methodName): MockObject | AccessUtils {
+        return $this->getMockBuilder(AccessUtils::class)
+            ->setConstructorArgs([$this->roleHierarchy,  $this->accessRepository, $this->optionalsRolesIterator])
+            ->setMethodsExcept([$methodName])
+            ->getMock();
+    }
+
+
     /**
      * @see Utils::filterAccesses()
      */
     public function testFilterAccesses(): void
     {
-        $accessUtils = $this->getMockBuilder(AccessUtils::class)
-            ->setConstructorArgs([$this->roleHierarchy,  $this->accessRepository, $this->optionalsRolesIterator])
-            ->setMethodsExcept(['filterAccesses'])
-            ->getMock();
+        $accessUtils = $this->buildAccessUtilsMockForMethod('filterAccesses');
 
         $access1 = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
         $access1->method('getId')->willReturn(1);
@@ -58,10 +73,7 @@ class UtilsTest extends TestCase
      */
     public function testHasRole(): void
     {
-        $accessUtils = $this->getMockBuilder(AccessUtils::class)
-            ->setConstructorArgs([$this->roleHierarchy,  $this->accessRepository, $this->optionalsRolesIterator])
-            ->setMethodsExcept(['hasRole'])
-            ->getMock();
+        $accessUtils = $this->buildAccessUtilsMockForMethod('hasRole');
 
         $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
 
@@ -76,10 +88,7 @@ class UtilsTest extends TestCase
      */
     public function testGetAllRoles(): void
     {
-        $accessUtils = $this->getMockBuilder(AccessUtils::class)
-            ->setConstructorArgs([$this->roleHierarchy,  $this->accessRepository, $this->optionalsRolesIterator])
-            ->setMethodsExcept(['getAllRoles'])
-            ->getMock();
+        $accessUtils = $this->buildAccessUtilsMockForMethod('getAllRoles');
 
         $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
         $access->method('getRoles')->willReturn(['ROLE_A']);
@@ -95,10 +104,7 @@ class UtilsTest extends TestCase
      */
     public function testFindAdminFor(): void
     {
-        $accessUtils = $this->getMockBuilder(AccessUtils::class)
-            ->setConstructorArgs([$this->roleHierarchy,  $this->accessRepository, $this->optionalsRolesIterator])
-            ->setMethodsExcept(['findAdminFor'])
-            ->getMock();
+        $accessUtils = $this->buildAccessUtilsMockForMethod('findAdminFor');
 
         $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
         $organization = $this->getMockBuilder(Organization::class)->disableOriginalConstructor()->getMock();
@@ -113,10 +119,7 @@ class UtilsTest extends TestCase
      */
     public function testFindAdminForNotFound(): void
     {
-        $accessUtils = $this->getMockBuilder(AccessUtils::class)
-            ->setConstructorArgs([$this->roleHierarchy,  $this->accessRepository, $this->optionalsRolesIterator])
-            ->setMethodsExcept(['findAdminFor'])
-            ->getMock();
+        $accessUtils = $this->buildAccessUtilsMockForMethod('findAdminFor');
 
         $organization = $this->getMockBuilder(Organization::class)->disableOriginalConstructor()->getMock();
 
@@ -124,4 +127,111 @@ class UtilsTest extends TestCase
 
         $this->assertNull($accessUtils->findAdminFor($organization));
     }
+
+    public function testGetActiveFunctions(): void {
+        $accessUtils = $this->buildAccessUtilsMockForMethod('getActiveFunctions');
+
+        DatesUtils::setFakeDatetime('2023-01-01 12:00');
+
+        $access = $this->getMockBuilder(Access::class)->getMock();
+
+        // Valide: pas de date de fin ou de début
+        $functionType1 = $this->getMockBuilder(FunctionType::class)->getMock();
+        $functionType1->method('getMission')->willReturn('Mission1');
+
+        $function1 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
+        $function1->method('getEndDate')->willReturn(null);
+        $function1->method('getStartDate')->willReturn(null);
+        $function1->method('getFunctionType')->willReturn($functionType1);
+
+        // Valide: pas de date de fin ou de début
+        $functionType2 = $this->getMockBuilder(FunctionType::class)->getMock();
+        $functionType2->method('getMission')->willReturn('Mission2');
+
+        $function2 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
+        $function2->method('getEndDate')->willReturn(null);
+        $function2->method('getStartDate')->willReturn(null);
+        $function2->method('getFunctionType')->willReturn($functionType2);
+
+        // Invalide: date de fin dépassée
+        $functionType3 = $this->getMockBuilder(FunctionType::class)->getMock();
+        $functionType3->method('getMission')->willReturn('Mission3');
+
+        $function3 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
+        $function3->method('getEndDate')->willReturn(new \DateTime('2022-01-01'));
+        $function3->method('getStartDate')->willReturn(null);
+        $function3->method('getFunctionType')->willReturn($functionType3);
+
+        // Valide : date de fin dans les 24h suivant le jour même
+        $functionType4 = $this->getMockBuilder(FunctionType::class)->getMock();
+        $functionType4->method('getMission')->willReturn('Mission4');
+
+        $function4 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
+        $function4->method('getEndDate')->willReturn(new \DateTime('2023-01-01 18:00'));
+        $function4->method('getStartDate')->willReturn(null);
+        $function4->method('getFunctionType')->willReturn($functionType4);
+
+        // Valide: date de fin dans le futur
+        $functionType5 = $this->getMockBuilder(FunctionType::class)->getMock();
+        $functionType5->method('getMission')->willReturn('Mission5');
+
+        $function5 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
+        $function5->method('getEndDate')->willReturn(new \DateTime('2023-06-01'));
+        $function5->method('getStartDate')->willReturn(null);
+        $function5->method('getFunctionType')->willReturn($functionType5);
+
+        // Invalide: date de début dans le futur
+        $functionType6 = $this->getMockBuilder(FunctionType::class)->getMock();
+        $functionType6->method('getMission')->willReturn('Mission6');
+
+        $function6 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
+        $function6->method('getEndDate')->willReturn(null);
+        $function6->method('getStartDate')->willReturn(new \DateTime('2023-06-01'));
+        $function6->method('getFunctionType')->willReturn($functionType6);
+
+        // Valide: date de début dans le passé
+        $functionType7 = $this->getMockBuilder(FunctionType::class)->getMock();
+        $functionType7->method('getMission')->willReturn('Mission7');
+
+        $function7 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
+        $function7->method('getEndDate')->willReturn(null);
+        $function7->method('getStartDate')->willReturn(new \DateTime('2022-06-01'));
+        $function7->method('getFunctionType')->willReturn($functionType7);
+
+        // Valide: date de début dans le passé et date de fin dans le futur
+        $functionType8 = $this->getMockBuilder(FunctionType::class)->getMock();
+        $functionType8->method('getMission')->willReturn('Mission8');
+
+        $function8 = $this->getMockBuilder(OrganizationFunction::class)->getMock();
+        $function8->method('getEndDate')->willReturn(new \DateTime('2023-06-01'));
+        $function8->method('getStartDate')->willReturn(new \DateTime('2022-06-01'));
+        $function8->method('getFunctionType')->willReturn($functionType8);
+
+        $access->method('getOrganizationFunction')->willReturn(
+            new ArrayCollection([$function1, $function2, $function3, $function4, $function5, $function6, $function7, $function8])
+        );
+
+        $this->assertEquals(
+            ['Mission1', 'Mission2', 'Mission4', 'Mission5', 'Mission7', 'Mission8'],
+            $accessUtils->getActiveFunctions($access)
+        );
+    }
+
+    public function testHasActiveFunctions(): void {
+        $accessUtils = $this->buildAccessUtilsMockForMethod('hasActiveFunction');
+
+        $access = $this->getMockBuilder(Access::class)->getMock();
+
+        $accessUtils
+            ->method('getActiveFunctions')
+            ->with($access)
+            ->willReturn(['Mission1', 'Mission2']);
+
+        $this->assertTrue(
+            $accessUtils->hasActiveFunction($access, 'Mission1')
+        );
+        $this->assertFalse(
+            $accessUtils->hasActiveFunction($access, 'Mission3')
+        );
+    }
 }

+ 15 - 14
tests/Unit/Service/Network/UtilsTest.php

@@ -10,6 +10,7 @@ use App\Service\Network\Utils as NetworkUtils;
 use DateTime;
 use Doctrine\Common\Collections\ArrayCollection;
 use PHPUnit\Framework\TestCase;
+use App\Service\Utils\DatesUtils;
 
 class UtilsTest extends TestCase
 {
@@ -186,7 +187,7 @@ class UtilsTest extends TestCase
     }
 
     /**
-     * @see Utils::doesOrganizationBelongToTheNetwork()
+     * @see Utils::isNetworkOrganizationActiveNow()
      */
     public function testIsOrganizationActiveNow():void
     {
@@ -195,22 +196,22 @@ class UtilsTest extends TestCase
             ->setMethodsExcept(['isNetworkOrganizationActiveNow'])
             ->getMock();
 
-        $date1 = new DateTime('now');
-        $date1->modify('-1 year');
+        DatesUtils::setFakeDatetime('2023-01-01');
 
-        $date2 = new DateTime('now');
-        $date2->modify('+1 year');
+        $dateNow = DatesUtils::new();
+        $startDate = DatesUtils::new('2022-01-01');
+        $endDate = DatesUtils::new('2024-01-01');
 
         $networkOrganization = $this->getMockBuilder(NetworkOrganization::class)->getMock();
         $networkOrganization
             ->expects(self::exactly(2))
             ->method('getStartDate')
-            ->willReturn($date1);
+            ->willReturn($startDate);
 
         $networkOrganization
             ->expects(self::once())
             ->method('getEndDate')
-            ->willReturn($date2);
+            ->willReturn($endDate);
 
         $this->assertTrue(
             $networkUtils->isNetworkOrganizationActiveNow($networkOrganization)
@@ -218,7 +219,7 @@ class UtilsTest extends TestCase
     }
 
     /**
-     * @see Utils::doesOrganizationBelongToTheNetwork()
+     * @see Utils::isNetworkOrganizationActiveNow()
      */
     public function testIsOrganizationActiveNowNotActive():void
     {
@@ -227,22 +228,22 @@ class UtilsTest extends TestCase
             ->setMethodsExcept(['isNetworkOrganizationActiveNow'])
             ->getMock();
 
-        $date1 = new DateTime('now');
-        $date1->modify('-3 year');
+        DatesUtils::setFakeDatetime('2023-01-01');
 
-        $date2 = new DateTime('now');
-        $date2->modify('-1 year');
+        $dateNow = DatesUtils::new();
+        $startDate = DatesUtils::new('2021-01-01');
+        $endDate = DatesUtils::new('2022-01-01');
 
         $networkOrganization = $this->getMockBuilder(NetworkOrganization::class)->getMock();
         $networkOrganization
             ->expects(self::exactly(2))
             ->method('getStartDate')
-            ->willReturn($date1);
+            ->willReturn($startDate);
 
         $networkOrganization
             ->expects(self::once())
             ->method('getEndDate')
-            ->willReturn($date2);
+            ->willReturn($endDate);
 
         $this->assertFalse(
             $networkUtils->isNetworkOrganizationActiveNow($networkOrganization)

+ 333 - 0
tests/Unit/Service/OnlineRegistration/RegistrationStatusServiceTest.php.off

@@ -0,0 +1,333 @@
+<?php
+
+namespace App\Tests\Unit\Service\OnlineRegistration;
+
+use App\Entity\Access\Access;
+use App\Entity\AccessWish\AccessFamilyWish;
+use App\Entity\AccessWish\AccessWish;
+use App\Entity\AccessWish\EducationStudentWish;
+use App\Enum\OnlineRegistration\RegistrationStatusEnum;
+use App\Enum\OnlineRegistration\WishRegistrationEnum;
+use App\Service\OnlineRegistration\RegistrationStatusService;
+use App\Service\Utils\DatesUtils;
+use Doctrine\Common\Collections\ArrayCollection;
+use PHPUnit\Framework\TestCase;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class TestableRegistrationStatusService extends RegistrationStatusService {
+    public function getCurrentAccessWish(Access $access): AccessWish | null { return parent::getCurrentAccessWish($access); }
+    public function getDaysSinceLastUpdate(AccessWish $currentAccessWish): int {
+        return parent::getDaysSinceLastUpdate($currentAccessWish);
+    }
+    public function countEducationsByRegistrationStatus(AccessWish $currentAccessWish): array | null {
+        return parent::countEducationsByRegistrationStatus($currentAccessWish);
+    }
+}
+
+
+class RegistrationStatusServiceTest extends TestCase
+{
+    public function tearDown(): void
+    {
+        DatesUtils::clearFakeDatetime();
+    }
+
+    private function makeRegistrationStatusServiceMockForMethod(string $methodName): MockObject | TestableRegistrationStatusService {
+        return $this->getMockBuilder(TestableRegistrationStatusService::class)
+            ->setMethodsExcept([$methodName])
+            ->getMock();
+    }
+
+    private function _getStatusWith(int $negotiable, int $accepted, int $pending, int $denied): string | null {
+        $registrationStatusService = $this->makeRegistrationStatusServiceMockForMethod('getStatus');
+
+        $access = $this->getMockBuilder(Access::class)->getMock();
+
+        $accessWish = $this->getMockBuilder(AccessWish::class)->getMock();
+        $accessFamilyWish = $this->getMockBuilder(AccessFamilyWish::class)->getMock();
+        $accessFamilyWish->method('isRegistrationCompleted')->willReturn(true);
+        $accessWish->method('getAccessFamilyWish')->willReturn($accessFamilyWish);
+
+        $registrationStatusService->method('getCurrentAccessWish')->with($access)->willReturn($accessWish);
+
+        $wishesCountByStatus = [
+            RegistrationStatusEnum::NEGOTIABLE()->getValue() => $negotiable,
+            RegistrationStatusEnum::ACCEPTED()->getValue() => $accepted,
+            RegistrationStatusEnum::PENDING()->getValue() => $pending,
+            RegistrationStatusEnum::DENIED()->getValue() => $denied
+        ];
+
+        $registrationStatusService
+            ->expects(self::once())
+            ->method('countEducationsByRegistrationStatus')
+            ->with($accessWish)
+            ->willReturn($wishesCountByStatus);
+
+        return $registrationStatusService->getStatus($access);
+    }
+
+
+    public function testGetStatusIsNegotiable(): void {
+        $this->assertEquals(
+            $this->_getStatusWith(1, 0, 0, 0),
+            RegistrationStatusEnum::NEGOTIABLE()->getValue()
+        );
+        $this->assertEquals(
+            RegistrationStatusEnum::NEGOTIABLE()->getValue(),
+            $this->_getStatusWith(1, 0, 0, 1)
+
+        );
+    }
+
+    public function testGetStatusIsPending(): void {
+        $this->assertEquals(
+            RegistrationStatusEnum::PENDING()->getValue(),
+            $this->_getStatusWith(0, 0, 1, 0)
+        );
+        $this->assertEquals(
+            RegistrationStatusEnum::PENDING()->getValue(),
+            $this->_getStatusWith(1, 0, 1, 0)
+        );
+    }
+
+    public function testGetStatusIsAccepted(): void {
+        $this->assertEquals(
+            RegistrationStatusEnum::ACCEPTED()->getValue(),
+            $this->_getStatusWith(0, 1, 0, 0)
+        );
+        $this->assertEquals(
+            RegistrationStatusEnum::ACCEPTED()->getValue(),
+            $this->_getStatusWith(1, 1, 1, 1)
+        );
+    }
+
+    public function testGetStatusIsDenied(): void {
+        $this->assertEquals(
+            RegistrationStatusEnum::DENIED()->getValue(),
+            $this->_getStatusWith(0, 0, 0, 1)
+        );
+    }
+
+    public function testGetStatusNoAccessWish(): void {
+        $registrationStatusService = $this->makeRegistrationStatusServiceMockForMethod('getStatus');
+        $access = $this->getMockBuilder(Access::class)->getMock();
+        $registrationStatusService->method('getCurrentAccessWish')->with($access)->willReturn(null);
+
+        $this->assertEquals(
+            null,
+            $registrationStatusService->getStatus($access)
+        );
+    }
+
+    public function testGetStatusIncompleteRegistration(): void {
+        $registrationStatusService = $this->makeRegistrationStatusServiceMockForMethod('getStatus');
+
+        $access = $this->getMockBuilder(Access::class)->getMock();
+        $accessWish = $this->getMockBuilder(AccessWish::class)->getMock();
+        $accessFamilyWish = $this->getMockBuilder(AccessFamilyWish::class)->getMock();
+        $accessFamilyWish->method('isRegistrationCompleted')->willReturn(false);
+        $accessWish->method('getAccessFamilyWish')->willReturn($accessFamilyWish);
+
+        $registrationStatusService->method('getCurrentAccessWish')->with($access)->willReturn($accessWish);
+
+        $this->assertEquals(
+            null,
+            $registrationStatusService->getStatus($access)
+        );
+    }
+
+    public function testGetStatusNoCountByStatuses(): void {
+        $registrationStatusService = $this->makeRegistrationStatusServiceMockForMethod('getStatus');
+
+        $access = $this->getMockBuilder(Access::class)->getMock();
+        $accessWish = $this->getMockBuilder(AccessWish::class)->getMock();
+        $accessFamilyWish = $this->getMockBuilder(AccessFamilyWish::class)->getMock();
+        $accessFamilyWish->method('isRegistrationCompleted')->willReturn(true);
+        $accessWish->method('getAccessFamilyWish')->willReturn($accessFamilyWish);
+
+        $registrationStatusService->method('getCurrentAccessWish')->with($access)->willReturn($accessWish);
+
+        $registrationStatusService
+            ->method('countEducationsByRegistrationStatus')
+            ->with($accessWish)
+            ->willReturn(null);
+
+        $this->assertEquals(
+            null,
+            $registrationStatusService->getStatus($access)
+        );
+    }
+
+    public function testGetStatusExpired(): void {
+        $registrationStatusService = $this->makeRegistrationStatusServiceMockForMethod('getStatus');
+
+        $access = $this->getMockBuilder(Access::class)->getMock();
+
+        $accessWish = $this->getMockBuilder(AccessWish::class)->getMock();
+        $accessFamilyWish = $this->getMockBuilder(AccessFamilyWish::class)->getMock();
+        $accessFamilyWish->method('isRegistrationCompleted')->willReturn(true);
+        $accessWish->method('getAccessFamilyWish')->willReturn($accessFamilyWish);
+
+        $registrationStatusService->method('getCurrentAccessWish')->with($access)->willReturn($accessWish);
+
+        $wishesCountByStatus = [
+            RegistrationStatusEnum::NEGOTIABLE()->getValue() => 0,
+            RegistrationStatusEnum::ACCEPTED()->getValue() => 1,
+            RegistrationStatusEnum::PENDING()->getValue() => 0,
+            RegistrationStatusEnum::DENIED()->getValue() => 0
+        ];
+
+        $registrationStatusService
+            ->method('countEducationsByRegistrationStatus')
+            ->with($accessWish)
+            ->willReturn($wishesCountByStatus);
+
+
+
+        $registrationStatusService
+            ->method('getDaysSinceLastUpdate')
+            ->with($accessWish)
+            ->willReturn($registrationStatusService::DISPLAYING_TIME + 1);
+
+        $this->assertEquals(
+            null,
+            $registrationStatusService->getStatus($access)
+        );
+    }
+
+    public function testGetStatusNoResult(): void {
+        $this->assertEquals(
+            null,
+            $this->_getStatusWith(0, 0, 0, 0)
+        );
+    }
+
+    public function testGetDaysSinceLastUpdate(): void {
+        $registrationStatusService = $this->makeRegistrationStatusServiceMockForMethod('getDaysSinceLastUpdate');
+
+        DatesUtils::setFakeDatetime('2023-01-11');
+
+        $accessWish = $this->getMockBuilder(AccessWish::class)->getMock();
+        $accessFamilyWish = $this->getMockBuilder(AccessFamilyWish::class)->getMock();
+        $accessFamilyWish->method('getUpdateDate')->willReturn(new \DateTime('2023-01-01'));
+        $accessWish->method('getAccessFamilyWish')->willReturn($accessFamilyWish);
+
+        $this->assertEquals(
+            10,
+            $registrationStatusService->getDaysSinceLastUpdate($accessWish)
+        );
+    }
+
+    public function testGetCurrentAccessWish(): void {
+
+        $registrationStatusService = $this->makeRegistrationStatusServiceMockForMethod('getCurrentAccessWish');
+
+        DatesUtils::setFakeDatetime('2023-01-01');
+
+        $access = $this->getMockBuilder(Access::class)->getMock();
+
+        $accessWish1 = $this->getMockBuilder(AccessWish::class)->getMock();
+        $accessWish1->method('getCreateDate')->willReturn(new \DateTime('2021-01-01'));
+
+        $accessWish2 = $this->getMockBuilder(AccessWish::class)->getMock();
+        $accessWish2->method('getCreateDate')->willReturn(new \DateTime('2022-01-01'));
+
+        $accessWish3 = $this->getMockBuilder(AccessWish::class)->getMock();
+        $accessWish3->method('getCreateDate')->willReturn(new \DateTime('2023-01-01'));
+
+        $access->method('getAccessWishes')->willReturn(new ArrayCollection([$accessWish1, $accessWish2, $accessWish3]));
+
+        $this->assertEquals(
+            $accessWish3,
+            $registrationStatusService->getCurrentAccessWish($access)
+        );
+    }
+
+    public function testGetCurrentAccessWishNoResult(): void {
+
+        $registrationStatusService = $this->makeRegistrationStatusServiceMockForMethod('getCurrentAccessWish');
+
+        DatesUtils::setFakeDatetime('2023-01-01');
+
+        $access = $this->getMockBuilder(Access::class)->getMock();
+
+        $accessWish1 = $this->getMockBuilder(AccessWish::class)->getMock();
+        $accessWish1->method('getCreateDate')->willReturn(new \DateTime('2021-01-01'));
+
+        $accessWish2 = $this->getMockBuilder(AccessWish::class)->getMock();
+        $accessWish2->method('getCreateDate')->willReturn(new \DateTime('2022-01-01'));
+
+        $access->method('getAccessWishes')->willReturn(new ArrayCollection([$accessWish1, $accessWish2]));
+
+        $this->assertEquals(
+            null,
+            $registrationStatusService->getCurrentAccessWish($access)
+        );
+    }
+
+    public function testCountEducationsByRegistrationStatus(): void {
+        $registrationStatusService = $this->makeRegistrationStatusServiceMockForMethod('countEducationsByRegistrationStatus');
+
+        $accessWish = $this->getMockBuilder(AccessWish::class)->getMock();
+
+        $educationStudentWish1 = $this->getMockBuilder(EducationStudentWish::class)->getMock();
+        $educationStudentWish1->expects(self::once())->method('getRegistrationStatus')->willReturn(RegistrationStatusEnum::NEGOTIABLE()->getValue());
+
+        $educationStudentWish2 = $this->getMockBuilder(EducationStudentWish::class)->getMock();
+        $educationStudentWish2->expects(self::once())->method('getRegistrationStatus')->willReturn(RegistrationStatusEnum::PENDING()->getValue());
+
+        $educationStudentWish3 = $this->getMockBuilder(EducationStudentWish::class)->getMock();
+        $educationStudentWish3->expects(self::once())->method('getRegistrationStatus')->willReturn(RegistrationStatusEnum::ACCEPTED()->getValue());
+
+        $accessWish
+            ->method('getEducationStudentWishes')
+            ->willReturn(new ArrayCollection([$educationStudentWish1, $educationStudentWish2, $educationStudentWish3]));
+
+        $reregistrationWish1 = $this->getMockBuilder(EducationStudentWish::class)->getMock();
+        $reregistrationWish1->method('getWishRegistration')->willReturn(WishRegistrationEnum::REREGISTER()->getValue());
+        $reregistrationWish1->expects(self::once())->method('getRegistrationStatus')->willReturn(RegistrationStatusEnum::ACCEPTED()->getValue());
+
+        $reregistrationWish2 = $this->getMockBuilder(EducationStudentWish::class)->getMock();
+        $reregistrationWish2->method('getWishRegistration')->willReturn(WishRegistrationEnum::REREGISTER()->getValue());
+        $reregistrationWish2->expects(self::once())->method('getRegistrationStatus')->willReturn(RegistrationStatusEnum::DENIED()->getValue());
+
+        $reregistrationWish3 = $this->getMockBuilder(EducationStudentWish::class)->getMock();
+        $reregistrationWish3->method('getWishRegistration')->willReturn(WishRegistrationEnum::NO_INFORMATION()->getValue());
+        $reregistrationWish3->expects(self::never())->method('getRegistrationStatus');
+
+        $accessWish
+            ->method('getEducationStudentReregistrationsWishes')
+            ->willReturn(new ArrayCollection([$reregistrationWish1, $reregistrationWish2, $reregistrationWish3]));
+
+        $expected = [
+            RegistrationStatusEnum::NEGOTIABLE()->getValue() => 1,
+            RegistrationStatusEnum::ACCEPTED()->getValue() => 2,
+            RegistrationStatusEnum::PENDING()->getValue() => 1,
+            RegistrationStatusEnum::DENIED()->getValue() => 1
+        ];
+
+        $this->assertEquals(
+            $expected,
+            $registrationStatusService->countEducationsByRegistrationStatus($accessWish)
+        );
+    }
+
+    public function testCountEducationsByRegistrationStatusNoWishes(): void {
+        $registrationStatusService = $this->makeRegistrationStatusServiceMockForMethod('countEducationsByRegistrationStatus');
+
+        $accessWish = $this->getMockBuilder(AccessWish::class)->getMock();
+
+        $accessWish
+            ->method('getEducationStudentWishes')
+            ->willReturn(new ArrayCollection([]));
+
+        $accessWish
+            ->method('getEducationStudentReregistrationsWishes')
+            ->willReturn(new ArrayCollection([]));
+
+        $this->assertEquals(
+            null,
+            $registrationStatusService->countEducationsByRegistrationStatus($accessWish)
+        );
+    }
+}

+ 43 - 0
tests/Unit/Service/Utils/ArrayUtilsTest.php

@@ -7,6 +7,34 @@ use PHPUnit\Framework\TestCase;
 
 class ArrayUtilsTest extends TestCase
 {
+    public function testAll(): void {
+
+        $isEven = function ($num) { return $num % 2 === 0; };
+
+        $this->assertFalse(
+            ArrayUtils::all([1, 2, 3], $isEven)
+        );
+
+        $this->assertTrue(
+            ArrayUtils::all([2, 4, 6], $isEven)
+        );
+    }
+
+    public function testAny(): void {
+
+        $isEven = function ($num) { return $num % 2 === 0; };
+
+        $this->assertFalse(
+            ArrayUtils::any([1, 3, 5], $isEven)
+        );
+
+        $this->assertTrue(
+            ArrayUtils::any([1, 2, 3], $isEven)
+        );
+    }
+
+
+
     /**
      * @øee ArrayUtils::getChanges()
      */
@@ -81,4 +109,19 @@ class ArrayUtilsTest extends TestCase
             )
         );
     }
+
+    public function testGetAndCast(): void {
+        $this->assertEquals(
+            123,
+            ArrayUtils::getAndCast(["a" => "123"], 'a', 'int')
+        );
+    }
+
+    public function testGetAndCastMissingKey(): void {
+        $this->assertEquals(
+            null,
+            ArrayUtils::getAndCast(["a" => "123"], 'b', 'int')
+        );
+    }
+
 }

+ 24 - 9
tests/Unit/Service/Utils/DatesUtilsTest.php

@@ -1,35 +1,41 @@
 <?php
 namespace App\Tests\Unit\Service\Utils;
 
+use _PHPStan_532094bc1\Nette\Utils\DateTime;
 use App\Service\Utils\DatesUtils;
 use PHPUnit\Framework\TestCase;
 
 class DatesUtilsTest extends TestCase
 {
+    public function tearDown(): void
+    {
+        DatesUtils::clearFakeDatetime();
+    }
+
     /**
-     * @see DatesUtils::isIntervalIsValidNow()
+     * @see DatesUtils::isNowInInterval()
      * @noinspection PhpRedundantOptionalArgumentInspection
      */
-    public function testIsIntervalIsValidNow():void
+    public function testIsNowInInterval():void
     {
-        $this->assertTrue(DatesUtils::isIntervalIsValidNow(new \DateTime('2020-01-02'), new \DateTime('2025-01-02')));
-        $this->assertTrue(DatesUtils::isIntervalIsValidNow(new \DateTime('2020-01-02'), null));
+        DatesUtils::setFakeDatetime('2023-01-01');
+        $this->assertTrue(DatesUtils::isNowInInterval(new \DateTime('2020-01-02'), new \DateTime('2025-01-02')));
+        $this->assertTrue(DatesUtils::isNowInInterval(new \DateTime('2020-01-02'), null));
     }
 
     /**
-     * @see DatesUtils::isIntervalIsValidNow()
+     * @see DatesUtils::isNowInInterval()
      */
-    public function testIsIntervalIsNotValidNow():void
+    public function testIsNowInIntervalFalse():void
     {
-        $this->assertFalse(DatesUtils::isIntervalIsValidNow(new \DateTime('2019-01-02'), new \DateTime('2020-01-02')));
+        DatesUtils::setFakeDatetime('2023-01-01');
+        $this->assertFalse(DatesUtils::isNowInInterval(new \DateTime('2019-01-02'), new \DateTime('2020-01-02')));
     }
 
     /**
      * @see DatesUtils::new()
      */
     public function testNew(): void {
-        DatesUtils::clearFakeDatetime();
-
         self::assertEquals('2021-01-01', DatesUtils::new('2021-01-01')->format('Y-m-d'));
 
         $now = new \DateTime();
@@ -53,4 +59,13 @@ class DatesUtilsTest extends TestCase
             DatesUtils::new()->format('Y-m-d')
         );
     }
+
+    public function testGetDaysSinceLastUpdate(): void {
+        DatesUtils::setFakeDatetime('2023-01-21');
+
+        $this->assertEquals(
+            DatesUtils::daysSince(new \DateTime('2023-01-01')),
+            20
+        );
+    }
 }