Просмотр исходного кода

Merge branch 'feature/address-contact-point-pr' into 'develop'

Page adresse d'une organisation et autre modifications

See merge request opentalent/api!1
Guffon 4 лет назад
Родитель
Сommit
4ce3e4079e
39 измененных файлов с 987 добавлено и 73 удалено
  1. 36 0
      config/api_platform/Access/access.yaml
  2. 2 2
      config/opentalent/enum.yaml
  3. 4 1
      config/packages/api_platform.yaml
  4. 4 0
      config/packages/doctrine.yaml
  5. 8 2
      config/services.yaml
  6. 16 0
      src/Annotation/DateTimeConstraintAware.php
  7. 11 7
      src/Doctrine/Access/CurrentAccessExtension.php
  8. 2 2
      src/Doctrine/Access/CurrentUserPersonalizedListExtension.php
  9. 3 2
      src/Doctrine/Access/Extensions/AdminExtension.php
  10. 25 0
      src/Doctrine/Access/Extensions/DateTimeConstraintExtension.php
  11. 1 1
      src/Doctrine/Access/HandleCurrentAccessExtension.php
  12. 48 0
      src/Doctrine/Core/AllowedAddressPostalExtension.php
  13. 1 1
      src/Doctrine/Core/CurrentNotificationUserExtension.php
  14. 1 1
      src/Doctrine/Core/CurrentUserNotificationExtension.php
  15. 47 0
      src/Doctrine/Organization/CurrentOrganizationAddressPostalExtension.php
  16. 47 0
      src/Doctrine/Organization/CurrentOrganizationExtension.php
  17. 8 23
      src/Entity/Access/Access.php
  18. 3 0
      src/Entity/Access/OrganizationFunction.php
  19. 38 7
      src/Entity/Core/AddressPostal.php
  20. 0 1
      src/Entity/Core/ContactPoint.php
  21. 8 0
      src/Entity/Core/Country.php
  22. 1 1
      src/Entity/Organization/Organization.php
  23. 26 4
      src/Entity/Organization/OrganizationAddressPostal.php
  24. 57 1
      src/Entity/Person/Person.php
  25. 84 0
      src/Entity/Person/PersonAddressPostal.php
  26. 1 0
      src/Entity/Traits/ActivityPeriodTrait.php
  27. 0 9
      src/Enum/Core/ContactPointTypeEnum.php
  28. 1 1
      src/Enum/Organization/AddressPostalOrganizationTypeEnum.php
  29. 15 0
      src/Enum/Person/AddressPostalPersonTypeEnum.php
  30. 38 0
      src/EventListener/DoctrineFilter/DoctrineFilterListener.php
  31. 101 0
      src/Filter/DoctrineFilter/DateTimeFilter.php
  32. 41 0
      src/Filter/Person/FullNameFilter.php
  33. 7 1
      src/Repository/Access/AccessRepository.php
  34. 3 3
      src/Repository/Cotisation/CotisationApiResourcesRepository.php
  35. 16 0
      src/Repository/Person/PersonAddressPostalRepository.php
  36. 29 0
      src/Service/Organization/Utils.php
  37. 226 0
      src/Service/Utils/DateTimeConstraint.php
  38. 20 0
      src/Service/Utils/StringsUtils.php
  39. 8 3
      tests/Service/Cotisation/UtilsTest.php

+ 36 - 0
config/api_platform/Access/access.yaml

@@ -0,0 +1,36 @@
+App\Entity\Access\Access:
+  collectionOperations:
+    get: ~
+
+    cget_students:
+      method: GET
+      path: '/students'
+      security: 'is_granted("ROLE_USERS_VIEW")'
+
+    cget_admin:
+      method: GET
+      path: '/admin'
+
+    cget_access_person_ref:
+      method: GET
+      path: '/access_people'
+      normalization_context:
+        groups: ['access_people_ref']
+
+  itemOperations:
+    get:
+      security: '(is_granted("ROLE_USERS_VIEW") and object.getOrganization().getId() == user.getOrganization().getId()) or (object.getId() == user.getId())'
+
+    get_access_address:
+      method: GET
+      path: '/access_addresses/{id}'
+      requirements:
+        id : '\d+'
+      normalization_context:
+        groups: ['access_address', 'address']]
+      security: 'object.getOrganization().getId() == user.getOrganization().getId()'
+
+    put:
+      security: 'is_granted("ROLE_USERS") or (object.getId() == user.getId())'
+
+    delete: ~

+ 2 - 2
config/opentalent/enum.yaml

@@ -20,8 +20,6 @@ opentalent:
     family_situation: 'App\Enum\AccessSocial\FamilySituationEnum'
 
   #Core
-    address_postal_person: 'App\Enum\Core\AddressPostalPersonTypeEnum'
-    address_postal_organization: 'App\Enum\Organization\AddressPostalOrganizationTypeEnum'
     period: 'App\Enum\Core\PeriodEnum'
     action_status_type: 'App\Enum\Core\ActionStatusTypeEnum'
     control_type: 'App\Enum\Core\ControlTypeEnum'
@@ -65,6 +63,7 @@ opentalent:
     organization_bulletin_output: 'App\Enum\Organization\BulletinOutputEnum'
     organization_bulletin_send_to: 'App\Enum\Organization\SendToBulletinEnum'
     organization_setting_country: 'App\Enum\Organization\CountryEnum'
+    address_postal_organization: 'App\Enum\Organization\AddressPostalOrganizationTypeEnum'
 
   #Person
     person_gender: 'App\Enum\Person\GenderEnum'
@@ -72,6 +71,7 @@ opentalent:
     person_medal_type: 'App\Enum\Person\MedalTypeEnum'
     person_moral_type: 'App\Enum\Person\MoralPersonTypeEnum'
     medal_type: 'App\Enum\Person\MedalTypeEnum'
+    address_postal_person: 'App\Enum\Person\AddressPostalPersonTypeEnum'
 
   #Place
     place_day_of_week: 'App\Enum\Place\DayOfWeekEnum'

+ 4 - 1
config/packages/api_platform.yaml

@@ -2,7 +2,10 @@ api_platform:
     enable_swagger_ui: false
     enable_re_doc: false
     mapping:
-        paths: ['%kernel.project_dir%/src/Entity', '%kernel.project_dir%/src/ApiResources']
+        paths:
+            - '%kernel.project_dir%/src/Entity'
+            - '%kernel.project_dir%/src/ApiResources'
+            - '%kernel.project_dir%/config/api_platform'
     patch_formats:
         json: ['application/merge-patch+json']
     swagger:

+ 4 - 0
config/packages/doctrine.yaml

@@ -21,6 +21,10 @@ doctrine:
         auto_generate_proxy_classes: true
         entity_managers:
             default:
+                filters:
+                    date_time_filter:
+                        class: App\Filter\DoctrineFilter\DateTimeFilter
+                        enabled: true
                 connection: default
                 auto_mapping: true
                 mappings:

+ 8 - 2
config/services.yaml

@@ -37,7 +37,7 @@ services:
         App\Service\Access\OptionalsRolesInterface:
             tags: ['app.optionalsroles']
 
-    App\Doctrine\Access\HandleAccessExtension:
+    App\Doctrine\Access\HandleCurrentAccessExtension:
         - !tagged_iterator app.extensions.access
     App\Service\Access\HandleOptionalsRoles:
         - !tagged_iterator app.optionalsroles
@@ -47,4 +47,10 @@ services:
     App\Serializer\AccessContextBuilder:
         decorates: 'api_platform.serializer.context_builder'
         arguments: [ '@App\Serializer\AccessContextBuilder.inner' ]
-        autoconfigure: false
+        autoconfigure: false
+
+    #########################################
+    ##  LISTENER ##
+    App\EventListener\DoctrineFilter\DoctrineFilterListener:
+        tags:
+            - { name: kernel.event_listener, event: kernel.request }

+ 16 - 0
src/Annotation/DateTimeConstraintAware.php

@@ -0,0 +1,16 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Annotation;
+
+use Attribute;
+
+/**
+ * Classe DateTimeConstraintAware qui gère l'annotation pour le Doctrine filter
+ */
+#[Attribute(Attribute::TARGET_CLASS)]
+final class DateTimeConstraintAware
+{
+    public string $startDateFieldName;
+    public string $endDateFieldName;
+}

+ 11 - 7
src/Doctrine/Access/AccessExtension.php → src/Doctrine/Access/CurrentAccessExtension.php

@@ -11,12 +11,15 @@ use Doctrine\ORM\QueryBuilder;
 use Symfony\Component\Security\Core\Security;
 
 /**
- * Class AccessExtension : Filtre de sécurité par défaut pour une resource Access
+ * Class CurrentAccessExtension : Filtre de sécurité par défaut pour une resource Access
  * @package App\Doctrine\Access
  */
-final class AccessExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentAccessExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
 {
-    public function __construct(private Security $security, private HandleAccessExtension $handleAccessExtension)
+    public function __construct(
+        private Security $security,
+        private HandleCurrentAccessExtension $handleCurrentAccessExtension
+    )
     { }
 
     public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
@@ -34,13 +37,14 @@ final class AccessExtension implements QueryCollectionExtensionInterface, QueryI
         if (Access::class !== $resourceClass) {
             return;
         }
-
         /** @var Access $currentUser */
         $currentUser = $this->security->getUser();
         $rootAlias = $queryBuilder->getRootAliases()[0];
-        $queryBuilder->andWhere(sprintf('%s.organization = :current_organization', $rootAlias));
-        $queryBuilder->setParameter('current_organization', $currentUser->getOrganization());
+        $queryBuilder
+            ->andWhere(sprintf('%s.organization = :current_organization', $rootAlias))
+            ->setParameter('current_organization', $currentUser->getOrganization())
+        ;
 
-        $this->handleAccessExtension->addWhere($queryBuilder, $operationName);
+        $this->handleCurrentAccessExtension->addWhere($queryBuilder, $operationName);
     }
 }

+ 2 - 2
src/Doctrine/Access/PersonalizedListExtension.php → src/Doctrine/Access/CurrentUserPersonalizedListExtension.php

@@ -11,10 +11,10 @@ use Doctrine\ORM\QueryBuilder;
 use Symfony\Component\Security\Core\Security;
 
 /**
- * Class PersonalizedListExtension : Filtre de sécurité par défaut pour une resource PersonalizedList
+ * Class CurrentUserPersonalizedListExtension : Filtre de sécurité par défaut pour une resource PersonalizedList
  * @package App\Doctrine\Access
  */
-final class PersonalizedListExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentUserPersonalizedListExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
 {
     public function __construct(private Security $security)
     { }

+ 3 - 2
src/Doctrine/Access/Extensions/AdminExtension.php

@@ -15,7 +15,8 @@ class AdminExtension implements AccessExtensionInterface {
     public function addWhere(QueryBuilder $queryBuilder)
     {
         $rootAlias = $queryBuilder->getRootAliases()[0];
-        $queryBuilder->andWhere(sprintf('%s.adminAccess = :adminAccess', $rootAlias));
-        $queryBuilder->setParameter('adminAccess', true);
+        $queryBuilder
+            ->andWhere(sprintf('%s.adminAccess = :adminAccess', $rootAlias))
+            ->setParameter('adminAccess', true);
     }
 }

+ 25 - 0
src/Doctrine/Access/Extensions/DateTimeConstraintExtension.php

@@ -0,0 +1,25 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Doctrine\Access\Extensions;
+
+use App\Doctrine\Access\AccessExtensionInterface;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+class DateTimeConstraintExtension implements AccessExtensionInterface {
+    public function __construct(
+        private RequestStack $requestStack
+    ){
+    }
+    public function support(string $name): bool
+    {
+        return $this->requestStack->getMainRequest()->get('_time_constraint', true) == true;
+    }
+
+    public function addWhere(QueryBuilder $queryBuilder)
+    {
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder->innerJoin(sprintf('%s.organizationFunction', $rootAlias), 'organization_function');
+    }
+}

+ 1 - 1
src/Doctrine/Access/HandleAccessExtension.php → src/Doctrine/Access/HandleCurrentAccessExtension.php

@@ -5,7 +5,7 @@ namespace App\Doctrine\Access;
 
 use Doctrine\ORM\QueryBuilder;
 
-class HandleAccessExtension{
+class HandleCurrentAccessExtension{
     public function __construct(private iterable $extensions)
     { }
 

+ 48 - 0
src/Doctrine/Core/AllowedAddressPostalExtension.php

@@ -0,0 +1,48 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Doctrine\Core;
+
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use App\Entity\Access\Access;
+use App\Entity\Core\AddressPostal;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\Security\Core\Security;
+
+/**
+ * Class AllowedAddressPostalExtension : Filtre de sécurité par défaut pour une resource AddressPostal
+ * @package App\Doctrine\Core
+ */
+final class AllowedAddressPostalExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+{
+    public function __construct(private Security $security)
+    { }
+
+    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
+    {
+        $this->addWhere($queryBuilder, $resourceClass, $operationName);
+    }
+
+    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
+    {
+        $this->addWhere($queryBuilder, $resourceClass, $operationName);
+    }
+
+    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    {
+        if (AddressPostal::class !== $resourceClass) {
+            return;
+        }
+
+        /** @var Access $currentUser */
+        $currentUser = $this->security->getUser();
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder
+            ->innerJoin(sprintf('%s.organizationAddressPostal', $rootAlias), 'organization_address_postal')
+            ->andWhere('organization_address_postal.organization = :organization')
+            ->setParameter('organization', $currentUser->getOrganization())
+        ;
+    }
+}

+ 1 - 1
src/Doctrine/Core/NotificationUserExtension.php → src/Doctrine/Core/CurrentNotificationUserExtension.php

@@ -15,7 +15,7 @@ use Symfony\Component\Security\Core\Security;
  * Class NotificationExtension : Filtre de sécurité par défaut pour une resource Notification
  * @package App\Doctrine\Core
  */
-final class NotificationUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentNotificationUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
 {
     public function __construct(private Security $security)
     { }

+ 1 - 1
src/Doctrine/Core/NotificationExtension.php → src/Doctrine/Core/CurrentUserNotificationExtension.php

@@ -15,7 +15,7 @@ use Symfony\Component\Security\Core\Security;
  * Class NotificationExtension : Filtre de sécurité par défaut pour une resource Notification
  * @package App\Doctrine\Core
  */
-final class NotificationExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentUserNotificationExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
 {
     public function __construct(private Security $security)
     { }

+ 47 - 0
src/Doctrine/Organization/CurrentOrganizationAddressPostalExtension.php

@@ -0,0 +1,47 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Doctrine\Organization;
+
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use App\Entity\Access\Access;
+use App\Entity\Organization\OrganizationAddressPostal;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\Security\Core\Security;
+
+/**
+ * Class OrganizationAddressPosteExtension : Filtre de sécurité par défaut pour une resource OrganizationAddressPostal
+ * @package App\Doctrine\Core
+ */
+final class CurrentOrganizationAddressPostalExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+{
+    public function __construct(private Security $security)
+    { }
+
+    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
+    {
+        $this->addWhere($queryBuilder, $resourceClass, $operationName);
+    }
+
+    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
+    {
+        $this->addWhere($queryBuilder, $resourceClass, $operationName);
+    }
+
+    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    {
+        if (OrganizationAddressPostal::class !== $resourceClass) {
+            return;
+        }
+
+        /** @var Access $currentUser */
+        $currentUser = $this->security->getUser();
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder
+            ->andWhere(sprintf('%s.organization = :organization', $rootAlias))
+            ->setParameter('organization', $currentUser->getOrganization())
+        ;
+    }
+}

+ 47 - 0
src/Doctrine/Organization/CurrentOrganizationExtension.php

@@ -0,0 +1,47 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Doctrine\Organization;
+
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use App\Entity\Access\Access;
+use App\Entity\Organization\Organization;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\Security\Core\Security;
+
+/**
+ * Class CurrentOrganizationExtension : Filtre de sécurité par défaut pour une resource Organization
+ * @package App\Doctrine\Core
+ */
+final class CurrentOrganizationExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+{
+    public function __construct(private Security $security)
+    { }
+
+    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
+    {
+        $this->addWhere($queryBuilder, $resourceClass, $operationName);
+    }
+
+    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
+    {
+        $this->addWhere($queryBuilder, $resourceClass, $operationName);
+    }
+
+    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    {
+        if (Organization::class !== $resourceClass) {
+            return;
+        }
+
+        /** @var Access $currentUser */
+        $currentUser = $this->security->getUser();
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder
+            ->andWhere(sprintf('%s.id = :organization', $rootAlias))
+            ->setParameter('organization', $currentUser->getOrganization()->getId())
+        ;
+    }
+}

+ 8 - 23
src/Entity/Access/Access.php

@@ -3,6 +3,9 @@ declare(strict_types=1);
 
 namespace App\Entity\Access;
 
+use ApiPlatform\Core\Annotation\ApiFilter;
+use App\Filter\Person\FullNameFilter;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\BooleanFilter;
 use ApiPlatform\Core\Annotation\ApiResource;
 use ApiPlatform\Core\Annotation\ApiSubresource;
 use App\Entity\Billing\AccessIntangible;
@@ -23,36 +26,17 @@ use Symfony\Component\Serializer\Annotation\Groups;
 
 /**
  * Fais le lien entre une Person et une Organization
+ * @ApiResource @see : config/api_platform/Access/access.yaml
  */
-#[ApiResource(
-    collectionOperations:[
-        'cget_students'=> [
-            'method' => 'GET',
-            'path' => '/students',
-            'security' => 'is_granted("ROLE_USERS_VIEW")'
-        ],
-        'cget_admin'=> [
-            'method' => 'GET',
-            'path' => '/admin'
-        ],
-        'get'
-    ],
-    itemOperations: [
-        'get' => [
-            'security' => '(is_granted("ROLE_USERS_VIEW") and object.getOrganization().getId() == user.getOrganization().getId()) or (object.getId() == user.getId())'
-        ],
-        'put' => [
-            'security' => 'is_granted("ROLE_USERS") or (object.getId() == user.getId())'
-        ],
-        'delete'
-    ]
-)]
 #[ORM\Entity(repositoryClass: AccessRepository::class)]
+#[ApiFilter(BooleanFilter::class, properties: ['person.isPhysical'])]
+#[ApiFilter(FullNameFilter::class)]
 class Access implements UserInterface
 {
     #[ORM\Id]
     #[ORM\Column]
     #[ORM\GeneratedValue]
+    #[Groups("access_people_ref")]
     private ?int $id = null;
 
     #[ORM\Column(options: ['default' => false])]
@@ -68,6 +52,7 @@ class Access implements UserInterface
 
     #[ORM\ManyToOne(cascade: ['persist'])]
     #[ORM\JoinColumn(nullable: false)]
+    #[Groups(["access_people_ref", "access_address"])]
     private Person $person;
 
     #[ORM\ManyToOne]

+ 3 - 0
src/Entity/Access/OrganizationFunction.php

@@ -4,6 +4,7 @@ declare(strict_types=1);
 namespace App\Entity\Access;
 
 use ApiPlatform\Core\Annotation\ApiResource;
+use App\Annotation\DateTimeConstraintAware;
 use App\Entity\Traits\ActivityPeriodTrait;
 use App\Repository\Access\OrganizationFunctionRepository;
 use Doctrine\ORM\Mapping as ORM;
@@ -13,7 +14,9 @@ use Symfony\Component\Validator\Constraints as Assert;
  * Fonction d'un Access dans une Organization sur une période donnée
  */
 #[ApiResource]
+
 #[ORM\Entity(repositoryClass: OrganizationFunctionRepository::class)]
+#[DateTimeConstraintAware(startDateFieldName: "startDate", endDateFieldName: "endDate")]
 class OrganizationFunction
 {
     use ActivityPeriodTrait;

+ 38 - 7
src/Entity/Core/AddressPostal.php

@@ -3,48 +3,72 @@ declare(strict_types=1);
 
 namespace App\Entity\Core;
 
+use ApiPlatform\Core\Annotation\ApiResource;
 use App\Entity\Organization\OrganizationAddressPostal;
 use App\Repository\Core\AddressPostalRepository;
 use Doctrine\ORM\Mapping as ORM;
-
+use App\Entity\Person\PersonAddressPostal;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[ApiResource(
+    collectionOperations: [
+        "get"
+    ],
+    itemOperations: [
+        "get" => ["security" => "is_granted('ROLE_ORGANIZATION_VIEW') and object.getOrganizationAddressPostal().getOrganization().getId() == user.getOrganization().getId()"],
+    ]
+)]
 #[ORM\Entity(repositoryClass: AddressPostalRepository::class)]
 class AddressPostal
 {
     #[ORM\Id]
     #[ORM\Column]
     #[ORM\GeneratedValue]
+    #[Groups(["address", "access_address"])]
     private ?int $id = null;
 
     #[ORM\ManyToOne]
+    #[Groups("address")]
     private ?Country $addressCountry = null;
 
     #[ORM\Column(length: 100, nullable: true)]
+    #[Groups("address")]
     private ?string $addressCity = null;
 
     #[ORM\Column(length: 100, nullable: true)]
+    #[Groups("address")]
     private ?string $addressOwner = null;
 
     #[ORM\Column(length: 20, nullable: true)]
+    #[Groups("address")]
     private ?string $postalCode = null;
 
     #[ORM\Column(length: 255, nullable: true)]
+    #[Groups("address")]
     private ?string $streetAddress = null;
 
     #[ORM\Column(length: 255, nullable: true)]
+    #[Groups("address")]
     private ?string $streetAddressSecond = null;
 
     #[ORM\Column(length: 255, nullable: true)]
+    #[Groups("address")]
     private ?string $streetAddressThird = null;
 
     #[ORM\Column(nullable: true)]
+    #[Groups("address")]
     private ?float $latitude = null;
 
     #[ORM\Column(nullable: true)]
+    #[Groups("address")]
     private ?float $longitude = null;
 
-    #[ORM\OneToOne(mappedBy: 'addressPostal', cascade: ['persist', 'remove'])]
+    #[ORM\OneToOne(mappedBy: 'addressPostal')]
     private OrganizationAddressPostal $organizationAddressPostal;
 
+    #[ORM\OneToOne(mappedBy: 'addressPostal')]
+    private PersonAddressPostal $personAddressPostal;
+
     public function getId(): ?int
     {
         return $this->id;
@@ -165,13 +189,20 @@ class AddressPostal
 
     public function setOrganizationAddressPostal(OrganizationAddressPostal $organizationAddressPostal): self
     {
-        // set the owning side of the relation if necessary
-        if ($organizationAddressPostal->getAddressPostal() !== $this) {
-            $organizationAddressPostal->setAddressPostal($this);
-        }
-
         $this->organizationAddressPostal = $organizationAddressPostal;
 
         return $this;
     }
+
+    public function getPersonAddressPostal(): ?PersonAddressPostal
+    {
+        return $this->personAddressPostal;
+    }
+
+    public function setPersonAddressPostal(PersonAddressPostal $personAddressPostal): self
+    {
+        $this->personAddressPostal = $personAddressPostal;
+
+        return $this;
+    }
 }

+ 0 - 1
src/Entity/Core/ContactPoint.php

@@ -44,7 +44,6 @@ class ContactPoint
 
     #[ORM\Column(length: 255, nullable: true)]
     #[Assert\Email(message: 'invalid-email-format', mode: 'strict')]
-    #[Assert\Regex(pattern: '/^[a-zA-Z0-9._%-]{1,64}@[a-zA-Z0-9.-]{2,249}\.[a-zA-Z]{2,6}$/', message: 'email-error')]
     private ?string $email = null;
 
     #[ORM\Column(length: 255, nullable: true)]

+ 8 - 0
src/Entity/Core/Country.php

@@ -3,9 +3,17 @@ declare(strict_types=1);
 
 namespace App\Entity\Core;
 
+use ApiPlatform\Core\Annotation\ApiResource;
 use App\Repository\Core\CountryRepository;
 use Doctrine\ORM\Mapping as ORM;
 
+#[ApiResource(
+    collectionOperations: ['get'],
+    itemOperations: ['get'],
+    attributes:[
+        'pagination_enabled' => false
+    ]
+)]
 #[ORM\Entity(repositoryClass: CountryRepository::class)]
 class Country
 {

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

@@ -24,7 +24,7 @@ use Symfony\Component\Validator\Constraints as Assert;
             'security' => '(is_granted("ROLE_ORGANIZATION_VIEW") or is_granted("ROLE_ORGANIZATION")) and object.getId() == user.getOrganization().getId()'
         ],
         'put' => [
-            'security' => 'is_granted("ROLE_ORGANIZATION") and object.getId() == user.getOrganization()s.getId()'
+            'security' => 'is_granted("ROLE_ORGANIZATION") and object.getId() == user.getOrganization().getId()'
         ]
     ]
 )]

+ 26 - 4
src/Entity/Organization/OrganizationAddressPostal.php

@@ -9,25 +9,47 @@ use App\Repository\Organization\OrganizationAddressPostalRepository;
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Component\Validator\Constraints as Assert;
 
-#[ApiResource]
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[ApiResource(
+    collectionOperations: [
+        "get" => ["security" => "is_granted('ROLE_ORGANIZATION_VIEW')"],
+        "post"
+    ],
+    itemOperations: [
+        "get" => ["security" => "is_granted('ROLE_ORGANIZATION_VIEW') and object.getOrganization().getId() == user.getOrganization().getId()"],
+        "put" => ["security" => "object.getOrganization().getId() == user.getOrganization().getId()"],
+        "delete" => ["security" => "object.getOrganization().getId() == user.getOrganization().getId()"],
+    ],
+    attributes: ["security" => "is_granted('ROLE_ORGANIZATION')"],
+    denormalizationContext: [
+        'groups' => ['address'],
+    ],
+    normalizationContext: [
+        'groups' => ['address'],
+    ],
+)]
 #[ORM\Entity(repositoryClass: OrganizationAddressPostalRepository::class)]
 class OrganizationAddressPostal
 {
     #[ORM\Id]
     #[ORM\Column]
     #[ORM\GeneratedValue]
+    #[Groups("address")]
     private ?int $id = null;
 
     #[ORM\ManyToOne(inversedBy: 'organizationAddressPostals')]
     #[ORM\JoinColumn(nullable: false)]
-    private ?Organization $organization = null;
+    private Organization $organization;
 
     #[ORM\OneToOne(inversedBy: 'organizationAddressPostal', cascade: ['persist', 'remove'])]
     #[ORM\JoinColumn(nullable: false)]
-    private ?AddressPostal $addressPostal = null;
+    #[Groups("address")]
+    private AddressPostal $addressPostal;
 
     #[ORM\Column(length: 255)]
-    #[Assert\Choice(callback: ['\App\Enum\Core\AddressPostalTypeEnum', 'toArray'], message: 'invalid-address-postal-type')]
+    #[Assert\Choice(callback: ['\App\Enum\Organization\AddressPostalOrganizationTypeEnum', 'toArray'], message: 'invalid-address-postal-type')]
+    #[Groups("address")]
     private string $type;
 
     public function getId(): ?int

+ 57 - 1
src/Entity/Person/Person.php

@@ -13,11 +13,17 @@ use Doctrine\ORM\Mapping as ORM;
 use JetBrains\PhpStorm\Pure;
 use Symfony\Component\Security\Core\User\UserInterface;
 use Symfony\Component\Validator\Constraints as Assert;
+use Symfony\Component\Serializer\Annotation\Groups;
 
 /**
  * Personne physique ou morale
  */
-#[ApiResource]
+#[ApiResource(
+    collectionOperations: [],
+    itemOperations: [
+        "get" => ["security" => "is_granted('ROLE_USERS_VIEW')"],
+    ]
+)]
 #[ORM\Entity(repositoryClass: PersonRepository::class)]
 class Person implements UserInterface
 {
@@ -35,14 +41,20 @@ class Person implements UserInterface
     private ?string $password = null;
 
     #[ORM\Column(length: 255, nullable: true)]
+    #[Groups(["access_people_ref", "access_address"])]
     private ?string $name = null;
 
     #[ORM\Column(length: 255, nullable: true)]
+    #[Groups(["access_people_ref", "access_address"])]
     private ?string $givenName = null;
 
     #[ORM\ManyToMany(targetEntity: ContactPoint::class, mappedBy: 'person')]
     private Collection $contactPoints;
 
+    #[ORM\OneToMany( mappedBy: 'person', targetEntity: PersonAddressPostal::class, orphanRemoval: true)]
+    #[Groups("access_address")]
+    private Collection $personAddressPostal;
+
     #[ORM\Column(nullable: true)]
     #[Assert\Choice(callback: ['\App\Enum\Person\GenderEnum', 'toArray'], message: 'invalid-gender')]
     private ?string $gender = null;
@@ -51,9 +63,13 @@ class Person implements UserInterface
     #[ORM\JoinColumn(referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
     private ?File $image = null;
 
+    #[ORM\Column(options: ['default' => true])]
+    private bool $isPhysical = true;
+
     #[Pure] public function __construct()
     {
         $this->contactPoints = new ArrayCollection();
+        $this->personAddressPostal = new ArrayCollection();
     }
 
     public function getId(): ?int
@@ -189,4 +205,44 @@ class Person implements UserInterface
     {
         return $this->image;
     }
+
+    public function getPersonAddressPostal(): Collection
+    {
+        return $this->personAddressPostal;
+    }
+
+    public function addPersonAddressPostal(PersonAddressPostal $personAddressPostal): self
+    {
+        if (!$this->personAddressPostal->contains($personAddressPostal)) {
+            $this->personAddressPostal[] = $personAddressPostal;
+            $personAddressPostal->setPerson($this);
+        }
+
+        return $this;
+    }
+
+    public function removePersonAddressPostal(PersonAddressPostal $personAddressPostal): self
+    {
+        if ($this->personAddressPostal->removeElement($personAddressPostal)) {
+            // set the owning side to null (unless already changed)
+            if ($personAddressPostal->getPerson() === $this) {
+                $personAddressPostal->setPerson(null);
+            }
+        }
+
+        return $this;
+    }
+
+    public function getIsPhysical(): ?bool
+    {
+        return $this->isPhysical;
+    }
+
+    public function setAdminAccess(bool $isPhysical): self
+    {
+        $this->isPhysical = $isPhysical;
+
+        return $this;
+    }
+
 }

+ 84 - 0
src/Entity/Person/PersonAddressPostal.php

@@ -0,0 +1,84 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Entity\Person;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use App\Entity\Core\AddressPostal;
+use App\Repository\Person\PersonAddressPostalRepository;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * Lien entre une Person et une AddressPostal
+ */
+#[ApiResource(
+    collectionOperations: [],
+    itemOperations: [
+        "get" => ["security" => "is_granted('ROLE_USERS_VIEW')"],
+    ]
+)]
+#[ORM\Entity(repositoryClass: PersonAddressPostalRepository::class)]
+class PersonAddressPostal
+{
+    #[ORM\Id]
+    #[ORM\Column]
+    #[ORM\GeneratedValue]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'personAddressPostal')]
+    #[ORM\JoinColumn(nullable: false)]
+    private Person $person;
+
+    #[ORM\OneToOne(inversedBy: 'personAddressPostal', cascade: ['persist', 'remove'])]
+    #[ORM\JoinColumn(nullable: false)]
+    #[Groups("access_address")]
+    private AddressPostal $addressPostal;
+
+    #[ORM\Column(length: 255)]
+    #[Assert\Choice(callback: ['\App\Enum\Person\AddressPostalPersonTypeEnum', 'toArray'], message: 'invalid-address-postal-type')]
+    #[Groups("access_address")]
+    private string $type;
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getPerson(): ?Person
+    {
+        return $this->person;
+    }
+
+    public function setPerson(Person $person): self
+    {
+        $this->person = $person;
+
+        return $this;
+    }
+
+    public function getAddressPostal(): ?AddressPostal
+    {
+        return $this->addressPostal;
+    }
+
+    public function setAddressPostal(AddressPostal $addressPostal): self
+    {
+        $this->addressPostal = $addressPostal;
+
+        return $this;
+    }
+
+    public function getType(): string
+    {
+        return $this->type;
+    }
+
+    public function setType(string $type): self
+    {
+        $this->type = $type;
+
+        return $this;
+    }
+}

+ 1 - 0
src/Entity/Traits/ActivityPeriodTrait.php

@@ -3,6 +3,7 @@ declare(strict_types=1);
 
 namespace App\Entity\Traits;
 
+use App\Annotation\DateTimeAware;
 use Doctrine\ORM\Mapping as ORM;
 
 trait ActivityPeriodTrait

+ 0 - 9
src/Enum/Core/ContactPointTypeEnum.php

@@ -15,13 +15,4 @@ class ContactPointTypeEnum extends Enum
     private const BILL = 'BILL';
     private const OTHER = 'OTHER';
     private const CONTACT = 'CONTACT';
-
-    public static function toArray(bool $type = false): array
-    {
-        if($type == 'person'){
-            return ['PRINCIPAL'=>self::PRINCIPAL,'OTHER'=>self::OTHER];
-        }else{
-            return parent::toArray();
-        }
-    }
 }

+ 1 - 1
src/Enum/Organization/AddressPostalOrganizationTypeEnum.php

@@ -6,7 +6,7 @@ namespace App\Enum\Organization;
 use MyCLabs\Enum\Enum;
 
 /**
- * Type d'adresse postale
+ * Type d'adresse postale pour une organization
  */
 class AddressPostalOrganizationTypeEnum extends Enum
 {

+ 15 - 0
src/Enum/Person/AddressPostalPersonTypeEnum.php

@@ -0,0 +1,15 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Enum\Person;
+
+use MyCLabs\Enum\Enum;
+
+/**
+ * Type d'adresse postale pour une Person
+ */
+class AddressPostalPersonTypeEnum extends Enum
+{
+    private const ADDRESS_PRINCIPAL = 'ADDRESS_PRINCIPAL';
+    private const ADDRESS_OTHER = 'ADDRESS_OTHER';
+}

+ 38 - 0
src/EventListener/DoctrineFilter/DoctrineFilterListener.php

@@ -0,0 +1,38 @@
+<?php
+declare(strict_types=1);
+
+namespace App\EventListener\DoctrineFilter;
+
+use App\Service\Utils\DateTimeConstraint;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpKernel\Event\RequestEvent;
+use Symfony\Component\Security\Core\Security;
+
+/**
+ * Classe DoctrineFilterListener qui permet d'assurer l'injection de dépendance pour le SQL Filter
+ */
+class DoctrineFilterListener
+{
+    public function __construct(
+        private EntityManagerInterface $entityManager,
+        private DateTimeConstraint $dateTimeConstraint,
+        private Security $security,
+        private RequestStack $requestStack
+    )
+    {
+    }
+
+    public function onKernelRequest(RequestEvent $event)
+    {
+        if (!$event->isMainRequest()) {
+            // don't do anything if it's not the main request
+            return;
+        }
+        $filter = $this->entityManager->getFilters()->getFilter('date_time_filter');
+        $filter->setParameter('accessId', $this->security->getUser()?->getId() ?? null);
+        $filter->setParameter('_time_constraint', $this->requestStack->getMainRequest()->get('_time_constraint', true));
+
+        $filter->setDateTimeConstraint($this->dateTimeConstraint);
+    }
+}

+ 101 - 0
src/Filter/DoctrineFilter/DateTimeFilter.php

@@ -0,0 +1,101 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Filter\DoctrineFilter;
+
+use App\Annotation\DateTimeConstraintAware;
+use App\Service\Utils\DateTimeConstraint;
+use App\Service\Utils\StringsUtils;
+use Doctrine\ORM\Mapping\ClassMetadata;
+use Doctrine\ORM\Query\Filter\SQLFilter;
+
+/**
+ * Classe DateTimeFilter qui définie la requête SQL devant être ajoutée aux Entités possédant l'annotation DateTimeConstraintAware
+ */
+final class DateTimeFilter extends SQLFilter
+{
+    private DateTimeConstraint $dateTimeConstraint;
+
+    /**
+     * Méthode surchargée de SQLFilter
+     * @param ClassMetadata $targetEntity
+     * @param string $targetTableAlias
+     * @return string
+     * @throws \Exception
+     */
+    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
+    {
+        if(!$this->hasParameter('_time_constraint')
+            || !boolval(StringsUtils::unquote($this->getParameter('_time_constraint')))
+            || !$this->hasParameter('accessId')
+        )
+            return '';
+
+        $dateTimeConstraintAware = $targetEntity->getReflectionClass()->getAttributes(DateTimeConstraintAware::class)[0] ?? null;
+        $startFieldName = $dateTimeConstraintAware?->getArguments()['startDateFieldName'] ?? null;
+        $endFieldName = $dateTimeConstraintAware?->getArguments()['endDateFieldName'] ?? null;
+        if ($startFieldName === '' || is_null($startFieldName) || $endFieldName === '' || is_null($endFieldName)) {
+            return '';
+        }
+
+        $accessId = intval(StringsUtils::unquote($this->getParameter('accessId')));
+        $constraints = $this->dateTimeConstraint->invoke($accessId);
+
+        $fields = [
+            DateTimeConstraint::START_KEY => $startFieldName,
+            DateTimeConstraint::END_KEY => $endFieldName
+        ];
+      
+        return $this->constructQuery($constraints, $targetTableAlias, $fields);
+    }
+
+    /**
+     * Fonction permettant de construire la requête SQL correspondante aux contraintes
+     * @param array $constraints
+     * @param string $targetTableAlias
+     * @param array $fields
+     * @return string
+     */
+    private function constructQuery(array $constraints, string $targetTableAlias, array $fields): string{
+        $queryConditionsAND = [];
+        foreach ($constraints as $key => $constraint) {
+            $queryConditionsOR = [];
+            foreach ($constraint as $date => $conditions){
+                foreach ($conditions as $condition){
+                    $arithmetic = $this->getArithmeticValue($condition);
+                    if(!is_null($arithmetic))
+                        $queryConditionsOR[] = sprintf("%s.%s %s '%s'", $targetTableAlias, $fields[$key], $arithmetic, $date);
+                    else
+                        $queryConditionsOR[] = sprintf("%s.%s IS NULL", $targetTableAlias, $fields[$key]);
+                }
+            }
+            if(!empty($queryConditionsOR))
+                $queryConditionsAND[] = sprintf("(%s)", join(' OR ', $queryConditionsOR));
+        }
+        return join(" AND ", $queryConditionsAND);
+    }
+
+    /**
+     * Fonction retournant la valeur arithmétique correspondant à la condition de la contrainte
+     * @param $condition
+     * @return string|null
+     */
+    private function getArithmeticValue($condition): string|null{
+        switch ($condition){
+            case DateTimeConstraint::INF : return '<';
+            case DateTimeConstraint::EQUAL :  return '=';
+            case DateTimeConstraint::SUP :  return '>';
+            case DateTimeConstraint::INF + DateTimeConstraint::EQUAL : return '<=';
+            case DateTimeConstraint::SUP + DateTimeConstraint::EQUAL  : return '>=';
+        }
+        return null;
+    }
+
+    /**
+     * Permets d'assurer l'injection de dépendance du service DateTimeConstraint
+     * @param DateTimeConstraint $dateTimeConstraint
+     */
+    public function setDateTimeConstraint(DateTimeConstraint $dateTimeConstraint){
+        $this->dateTimeConstraint = $dateTimeConstraint;
+    }
+}

+ 41 - 0
src/Filter/Person/FullNameFilter.php

@@ -0,0 +1,41 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Filter\Person;
+
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractFilter;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use Doctrine\ORM\QueryBuilder;
+
+class FullNameFilter extends AbstractFilter{
+    protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
+    {
+        if ($property !== 'fullname') {
+            return;
+        }
+        $alias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder
+            ->innerJoin(sprintf('%s.person', $alias), 'person')
+            ->andWhere(sprintf('person.name LIKE :search OR person.givenName LIKE :search', $alias, $alias))
+            ->setParameter('search', '%'.$value.'%');
+    }
+
+    /**
+     * API docs
+     * @param string $resourceClass
+     * @return array[]
+     */
+    public function getDescription(string $resourceClass): array
+    {
+        return [
+            'fullname' => [
+                'property' => null,
+                'type' => 'string',
+                'required' => false,
+                'openapi' => [
+                    'description' => 'Rechercher parmi les champs name et givenName',
+                ],
+            ]
+        ];
+    }
+}

+ 7 - 1
src/Repository/Access/AccessRepository.php

@@ -95,6 +95,8 @@ class AccessRepository extends ServiceEntityRepository implements UserLoaderInte
 
     public function hasGotFunctionAtThisDate(Access $access, $function, \DateTime $date): bool
     {
+        $this->_em->getFilters()->disable('date_time_filter');
+
         $qb = $this->createQueryBuilder('access');
         $qb
             ->innerJoin('access.organizationFunction', 'organization_function')
@@ -106,6 +108,10 @@ class AccessRepository extends ServiceEntityRepository implements UserLoaderInte
         ;
         DateConditions::addDateInPeriodCondition($qb, 'organization_function', $date->format('Y-m-d'));
 
-        return count($qb->getQuery()->getResult()) > 0;
+        $result = count($qb->getQuery()->getResult()) > 0;
+
+        $this->_em->getFilters()->enable('date_time_filter');
+
+        return $result;
     }
 }

+ 3 - 3
src/Repository/Cotisation/CotisationApiResourcesRepository.php

@@ -7,7 +7,7 @@ namespace App\Repository\Cotisation;
 use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\Query\ResultSetMapping;
 
-final class CotisationApiResourcesRepository
+class CotisationApiResourcesRepository
 {
     public function __construct(private EntityManagerInterface $adminassosEntityManager)
     {
@@ -17,9 +17,9 @@ final class CotisationApiResourcesRepository
      * Récupère l'état de la cotisation pour une structure et une année
      * @param int $organizationId
      * @param int $year
-     * @return string|null
+     * @return int|null
      */
-    public function getAffiliationState(int $organizationId, int $year): string|null {
+    public function getAffiliationState(int $organizationId, int $year): int|null {
         $rsm = new ResultSetMapping();
         $rsm->addScalarResult('oa_miscellaneous_state_sta', 'oa_miscellaneous_state_sta');
 

+ 16 - 0
src/Repository/Person/PersonAddressPostalRepository.php

@@ -0,0 +1,16 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Repository\Person;
+
+use App\Entity\Person\PersonAddressPostal;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+final class PersonAddressPostalRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, PersonAddressPostal::class);
+    }
+}

+ 29 - 0
src/Service/Organization/Utils.php

@@ -14,6 +14,9 @@ use App\Test\Service\Organization\UtilsTest;
  */
 class Utils
 {
+    const START_DATE_KEY = 'dateStart';
+    const END_DATE_KEY = 'dateEnd';
+
     /**
      * Test si l'organisation est considérée comme une structure == n'a pas un produit manager
      * @param Organization $organization
@@ -86,4 +89,30 @@ class Utils
 
         else return $year;
     }
+
+    /**
+     * Fonction permettant de récupérer les dates de début et de fin d'activité d'une structure selon une année
+     * @param Organization $organization
+     * @param int $year
+     * @return \DateTime[]
+     * @throws \Exception
+     */
+    public static function getActivityPeriodsSwitchYear(Organization $organization, int $year): array
+    {
+        $musicalDate = $organization->getParameters()->getMusicalDate();
+
+        if (empty($musicalDate)) {
+            $dateStart = new \DateTime($year . "-09-01");
+            $dateEnd = new \DateTime(($year + 1) . "-08-31");
+        } else {
+            $dateStart = new \DateTime($year . "-" . $musicalDate->format('m') . "-" . $musicalDate->format('d'));
+            $dateEnd = clone($dateStart);
+            $dateEnd->add(new \DateInterval('P1Y'))->sub(new \DateInterval('P1D'));
+        }
+
+        return [
+            self::START_DATE_KEY => $dateStart->format('Y-m-d'),
+            self::END_DATE_KEY => $dateEnd->format('Y-m-d')
+        ];
+    }
 }

+ 226 - 0
src/Service/Utils/DateTimeConstraint.php

@@ -0,0 +1,226 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Utils;
+
+use App\Entity\Access\Access;
+use App\Service\Organization\Utils as organizationUtils;
+use Doctrine\ORM\EntityManagerInterface;
+
+/**
+ * Classe DateTimeConstraint qui définie les dates de débuts et fin de périodes
+ * par rapport au contraintes temporelles choisies par un utilisateur.
+ */
+class DateTimeConstraint
+{
+    const NULL = 0;
+    const INF = 1;
+    const EQUAL = 3;
+    const SUP = 5;
+    const CANCEL_OPERATION = 9;
+    const START_KEY = 'start';
+    const END_KEY = 'end';
+    const NULL_VALUE = 'NULL';
+
+    public function __construct(
+        private EntityManagerInterface $entityManager,
+    )
+    { }
+
+    /**
+     * Main méthode
+     * @param int $accessID
+     * @return array
+     * @throws \Exception
+     */
+    public function invoke(int $accessID): array
+    {
+        $access = $this->entityManager->getRepository(Access::class)->find($accessID);
+        $historical = $access->getHistorical();
+
+        $contraints = [
+            self::START_KEY => [],
+            self::END_KEY => []
+        ];
+
+        if($this->hasCustomPeriods($historical)){
+            $periods = $this->getCustomPeriods($historical['dateStart'], $historical['dateEnd']);
+            //Une période "Custom" reviens à savoir quels sont les éléments actif durant le "présent" de la période custom, donc
+            //on peut utiliser le presentConstraint avec les periods custom
+            $contraints = $this->addConstraint($contraints, $this->presentConstraint($periods));
+        }else{
+            $periods = $this->getPeriods($access);
+            if($historical['present']) $contraints = $this->addConstraint($contraints, $this->presentConstraint($periods));
+            if($historical['past']) $contraints = $this->addConstraint($contraints, $this->pastConstraint($periods));
+            if($historical['future']) $contraints = $this->addConstraint($contraints, $this->futurConstraint($periods));
+        }
+        return $this->cleanConstraints($contraints);
+    }
+
+    /**
+     * Retourne true si l'utilisateur veux une période précise
+     * @param $historical
+     * @return bool
+     */
+    private function hasCustomPeriods($historical): bool{
+        return $historical['dateStart'] && $historical['dateEnd'];
+    }
+
+    /**
+     * Retourne le tableau des périodes custom
+     * @param string $dateStart
+     * @param string $dateEnd
+     * @return string[]
+     */
+    private function getCustomPeriods(string $dateStart, string $dateEnd): array{
+        return [
+            organizationUtils::START_DATE_KEY => $dateStart,
+            organizationUtils::END_DATE_KEY => $dateEnd
+        ];
+    }
+
+    /**
+     * Fonction permettant de récupérer les périodes de début et de fin d'affichage
+     * @param Access $access
+     * @return array
+     * @throws \Exception
+     */
+    private function getPeriods(Access $access): array{
+        $organization = $access->getOrganization();
+        $activityYear = $access->getActivityYear();
+        $currentActivityYear = organizationUtils::getOrganizationCurrentActivityYear($organization);
+
+        $periods = organizationUtils::getActivityPeriodsSwitchYear($organization, $activityYear);
+        //Si l'année courante est l'année d'affichage choisie par l'utilisateur, alors la date de début est aujourd'hui
+        if($activityYear === $currentActivityYear){
+            $today = new \DateTime('now');
+            $periods[organizationUtils::START_DATE_KEY] = $today->format('Y-m-d');
+        }
+
+        return $periods;
+    }
+
+    /**
+     * Fonction permettant d'ajouter une nouvelle contrainte de date
+     * @param array $contraints
+     * @param array $newContraint
+     * @return array
+     */
+    private function addConstraint(array $contraints, array $newContraint): array{
+        $contraints = $this->mergeConstraint($contraints,$newContraint,self::START_KEY);
+        $contraints = $this->mergeConstraint($contraints,$newContraint,self::END_KEY);
+        return $contraints;
+    }
+
+    /**
+     * Construit le tableau de contraintes pour une condition (start, end)
+     * @param array $contraints
+     * @param array $newContraint
+     * @param string $key
+     * @return array
+     */
+    private function mergeConstraint(array $contraints, array $newContraint, string $key): array{
+        if(array_key_exists($key, $newContraint)){
+            foreach ($newContraint[$key] as $dateKey => $arithmeticValue){
+                //Si la date à déjà des conditions
+                if(array_key_exists($dateKey, $contraints[$key])){
+                    //Si la conditions (<, >, =, ...) n'est pas encore appliquée à la date
+                    if(!in_array($arithmeticValue, $contraints[$key][$dateKey]))
+                        $contraints[$key][$dateKey][] = $arithmeticValue;
+                }else{
+                    $contraints[$key][$dateKey] = [$arithmeticValue];
+                }
+            }
+        }
+
+        return $contraints;
+    }
+
+    /**
+     * Nettoyage des contraintes (toutes celles supérieur à la condition cancel et les valeurs null isolées)
+     * @param array $constraints
+     * @return array
+     */
+    private function cleanConstraints(array $constraints): array{
+        $constraints[self::START_KEY] = $this->filterConstraint($constraints, self::START_KEY);
+        $constraints[self::START_KEY] = $this->clearNull($constraints, self::START_KEY);
+
+        $constraints[self::END_KEY] = $this->filterConstraint($constraints, self::END_KEY);
+        $constraints[self::END_KEY] = $this->clearNull($constraints, self::END_KEY);
+        return $constraints;
+    }
+
+    /**
+     * Pour chaque contraintes appliquées à une date on vérifie qu'on ne dépasse pas la valeur cancel : c'est à dire
+     * la condition qui démontre que toutes les valeurs arithmétiques ont été choisies
+     * @param array $constraints
+     * @param $key
+     * @return array
+     */
+    private function filterConstraint(array $constraints, string $key): array{
+        return array_filter($constraints[$key], function($constraint){
+            return array_sum($constraint) < self::CANCEL_OPERATION ;
+        });
+    }
+
+    /**
+     * On ne conserve pas les contraintes sur des conditions start et end si ces dernieres ne possède qu'une valeur null :
+     * une valeur null doit obligatoirement s'appliquer avec un OR
+     * @param array $constraints
+     * @param $key
+     * @return array
+     */
+    private function clearNull(array $constraints, string $key): array{
+        if(count($constraints[$key]) === 1 && array_key_exists(self::NULL_VALUE, $constraints[$key]))
+            $constraints[$key] = [];
+        return $constraints[$key];
+    }
+
+    /**
+     * Une période est dans le présent si :
+     *  - la date de début est plus petite ou égale (<=) à la date de fin de période à afficher
+     * ET
+     *  - la date de fin est plus grande ou égale (>=) à la date de fin de période à afficher OU que la date de fin n'est pas remplie (NULL)
+     * @param $periods
+     * @return array
+     */
+    private function presentConstraint(array $periods): array{
+        return [
+          self::START_KEY => [
+              $periods[organizationUtils::END_DATE_KEY] => self::INF + self::EQUAL
+          ],
+          self::END_KEY => [
+              $periods[organizationUtils::START_DATE_KEY] => self::SUP + self::EQUAL,
+              self::NULL_VALUE => self::NULL
+          ]
+        ];
+    }
+
+    /**
+     * Une période est dans le passée si :
+     * - la date de fin est plus petite (<) que la date de début de période à afficher
+     * @param $periods
+     * @return \int[][]
+     */
+    private function pastConstraint($periods): array{
+        return [
+            self::END_KEY => [
+                $periods[organizationUtils::START_DATE_KEY] => self::INF
+            ]
+        ];
+    }
+
+    /**
+     * Une période est dans le future si :
+     * - la date de début est plus grande (>) que la date de fin de période à afficher
+     * @param $periods
+     * @return \int[][]
+     */
+    private function futurConstraint($periods): array{
+        return [
+            self::START_KEY => [
+                $periods[organizationUtils::END_DATE_KEY] => self::SUP
+            ]
+        ];
+    }
+}

+ 20 - 0
src/Service/Utils/StringsUtils.php

@@ -0,0 +1,20 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Utils;
+
+/**
+ * Class StringsUtils : méthodes d'aide pour la gestion de string.
+ * @package App\Service\Utils
+ */
+class StringsUtils
+{
+    /**
+     * Supprime les quotes d'une chaine de caractères
+     * @param string $str
+     * @return string
+     */
+    public static function unquote(string $str): string {
+        return str_replace("'", "", $str);
+    }
+}

+ 8 - 3
tests/Service/Cotisation/UtilsTest.php

@@ -22,10 +22,14 @@ class UtilsTest extends TestCase
     private OrganizationUtils $organizationUtilsMock;
 
     public function getOrganizationMock(): Organization{
-        $organizationMock = $this->getMockBuilder(Organization::class)->getMock();
-        return $organizationMock
+        $organizationMock = $this->getMockBuilder(Organization::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $organizationMock
             ->method('getId')
             ->willReturn(1);
+        return $organizationMock;
+
     }
 
     public function getUtilsInstance(){
@@ -59,6 +63,7 @@ class UtilsTest extends TestCase
         $this->cotisationApiResourcesRepositoryMock =
             $this
                 ->getMockBuilder(CotisationApiResourcesRepository::class)
+                ->disableOriginalConstructor()
                 ->getMock();
     }
 
@@ -327,7 +332,7 @@ class UtilsTest extends TestCase
 
         $this->cotisationApiResourcesRepositoryMock
             ->method('isNotDGVCustomer')
-            ->with($organizationMock->getId(), $year)
+            ->with($organizationMock->getId())
             ->willReturn(true);
 
         $this->assertEquals(AlertStateEnum::ADVERTISINGINSURANCE()->getValue(), $utils->getAlertState($organizationMock, $year) );