소스 검색

Merge branch 'hotfix/2.4.1' into develop

Olivier Massot 1 년 전
부모
커밋
73a44033ea

+ 1 - 6
.gitignore

@@ -34,11 +34,6 @@ symfony.lock
 
 /coverage/
 
-###> phpunit/phpunit ###
-/phpunit.xml
-.phpunit.result.cache
-###< phpunit/phpunit ###
-
 ###> phpstan ###
 .phpstan.neon
 ###< phpstan ###
@@ -62,4 +57,4 @@ phpstan.json
 /.php-cs-fixer.cache
 ###< friendsofphp/php-cs-fixer ###
 
-phpstan.json
+.php-cs-fixer.cache

+ 25 - 8
config/opentalent/modulesbyconditions.yaml

@@ -21,35 +21,35 @@ parameters:
                 - ROLE_COTISATION
             conditions:
                 service:
-                    name: App\Service\Cotisation\Utils
+                    name: App\Service\Organization\Utils
                     function: isLastParentAndCMF
         CotisationStructure:
             roles:
                 - ROLE_COTISATION
             conditions:
                 service:
-                    name: App\Service\Cotisation\Utils
+                    name: App\Service\Organization\Utils
                     function: isStructureAndCMF
         CotisationRate:
             roles:
                 - ROLE_COTISATION
             conditions:
                 service:
-                    name: App\Service\Cotisation\Utils
+                    name: App\Service\Organization\Utils
                     function: isManagerAndLastParentAndCMF
         CotisationTransmissionState:
             roles:
                 - ROLE_COTISATION
             conditions:
                 service:
-                    name: App\Service\Cotisation\Utils
+                    name: App\Service\Organization\Utils
                     function: isManagerAndCMF
         CotisationTransmission:
             roles:
                 - ROLE_COTISATION
             conditions:
                 service:
-                    name: App\Service\Cotisation\Utils
+                    name: App\Service\Organization\Utils
                     function: isManagerAndNotLastParentAndCMF
         CotisationCMFAdministration:
             roles:
@@ -57,7 +57,7 @@ parameters:
             conditions:
                 service:
                     name: App\Service\Organization\Utils
-                    function: isOrganizationCMF
+                    function: isCMF
         Admin2IOS:
             roles:
                 - ROLE_ADMIN2IOS
@@ -68,12 +68,12 @@ parameters:
         StatisticFederation:
             conditions:
                 service:
-                    name: App\Service\Cotisation\Utils
+                    name: App\Service\Organization\Utils
                     function: isManagerAndNotLastParentAndCMF
         StatisticStructure:
             conditions:
                 service:
-                    name: App\Service\Cotisation\Utils
+                    name: App\Service\Organization\Utils
                     function: isManagerAndCMF
         Network:
             conditions:
@@ -90,3 +90,20 @@ parameters:
                 service:
                     name: App\Service\Network\Utils
                     function: isCMF
+        AccessReward:
+            conditions:
+                service:
+                    name: App\Service\Network\Utils
+                    function: isCMF
+        AccessRewardCommand:
+            conditions:
+                service:
+                    name: App\Service\Organization\Utils
+                    function: isStructureAndCMF
+        Reward:
+            roles:
+                - ROLE_REWARD
+            conditions:
+                service:
+                    name: App\Service\Organization\Utils
+                    function: isManagerAndCMF

+ 12 - 7
config/opentalent/products.yaml

@@ -76,12 +76,6 @@ parameters:
           - Course
           - Education
 
-      Medals:
-        entities:
-          - Medal
-        roles:
-          - ROLE_MEDALS
-
       Donors:
         entities:
           - Donor
@@ -272,6 +266,18 @@ parameters:
         entities:
           - DolibarrAccount
 
+      AccessReward:
+        roles:
+          - ROLE_ACCESSREWARD
+        entities:
+          - AccessReward
+
+      Reward:
+        roles:
+          - ROLE_REWARD
+        entities:
+          - Reward
+
   opentalent.products:
       artist:
         modules:
@@ -280,7 +286,6 @@ parameters:
           - Events
           - GeneralConfig
           - Equipments
-          - Medals
           - Donors
           - Commissons
           - Website

+ 1 - 0
config/packages/security.yaml

@@ -25,6 +25,7 @@ security:
             - ROLE_ONLINEREGISTRATION_ADMINISTRATION
             - ROLE_STATISTIC
             - ROLE_ADMIN_CORE
+            - ROLE_REWARD
 
         ROLE_ADMIN_CORE: *BASE_ROLE_ADMINISTRATION_CORE
 

+ 1 - 1
config/packages/security/core.yaml

@@ -3,7 +3,7 @@ security:
         ROLE_CORE-CRUD:
           - ROLE_USERS
           - ROLE_COMMISSIONS
-          - ROLE_MEDALS
+          - ROLE_ACCESSREWARD
           - ROLE_DONORS
           - ROLE_MAILS
           - ROLE_EMAILS

+ 0 - 10
config/packages/security/medals.yaml

@@ -1,10 +0,0 @@
-security:
-    role_hierarchy:
-        ROLE_MEDALS:
-          - ROLE_MEDALS_VIEW
-
-        ROLE_MEDALS_VIEW:
-          - ROLE_MEDAL_REFERENCE
-
-        ROLE_MEDAL_REFERENCE:
-          - ROLE_ACCESS_REFERENCE

+ 10 - 0
config/packages/security/rewards.yaml

@@ -0,0 +1,10 @@
+security:
+    role_hierarchy:
+        ROLE_ACCESSREWARD:
+          - ROLE_ACCESSREWARD_VIEW
+
+        ROLE_ACCESSREWARD_VIEW:
+          - ROLE_ACCESSREWARD_REFERENCE
+
+        ROLE_ACCESSREWARD_REFERENCE:
+          - ROLE_ACCESSREWARD_REFERENCE

+ 4 - 0
env/.env.prod

@@ -34,3 +34,7 @@ BIND_FILE_BUFFER_FILE=/env/subdomain.txt
 ###> filename log ###
 LOG_FILE_NAME=prod
 ###< filename log ###
+
+###> mailcatcher ###
+MAILER_DSN=smtp://localhost:25?verify_peer=false
+###< symfony/mercure-bundle ###

+ 0 - 8
phpstan.dist.neon

@@ -1,8 +0,0 @@
-parameters:
-    level: 6
-    paths:
-        - bin/
-        - config/
-        - public/
-        - src/
-        - tests/

+ 48 - 48
sql/schema-extensions/001-view_public_events.sql

@@ -1,29 +1,29 @@
 CREATE OR REPLACE VIEW view_public_events AS
     SELECT
-        b.uuid, 
-        b.organization_id AS organizationId, 
-        b.name, 
-        b.description, 
-        b.url, 
-        b.datetimeStart, 
-        b.datetimeEnd, 
+        b.uuid,
+        b.organization_id AS organizationId,
+        b.name,
+        b.description,
+        b.url,
+        b.datetimeStart,
+        b.datetimeEnd,
         b.gender_id as gender,
-        COALESCE(b.priceMini, 0) AS priceMini, 
-        COALESCE(b.priceMaxi, 0) AS priceMaxi, 
+        COALESCE(b.priceMini, 0) AS priceMini,
+        COALESCE(b.priceMaxi, 0) AS priceMaxi,
         null as categoryCode,
-        p.name AS placeName, 
-        p.description AS placeDescription, 
-        p.floorSize AS placeFloorSize, 
+        p.name AS placeName,
+        p.description AS placeDescription,
+        p.floorSize AS placeFloorSize,
         p.capacity AS placeCapacity,
-        ap.addressCity AS city, 
+        ap.addressCity AS city,
         ap.postalCode,
         TRIM(BOTH ' ' FROM CONCAT( IFNULL(ap.streetAddress, ''), ' ', IFNULL(ap.streetAddressSecond, ''), ' ', IFNULL(ap.streetAddressThird, ''))) AS streetAddress,
-        ap.longitude, 
+        ap.longitude,
         ap.latitude,
-        r.name AS roomName, 
-        r.description AS roomDescription, 
-        r.localisation AS roomLocalisation, 
-        r.capacity AS roomCapacity, 
+        r.name AS roomName,
+        r.description AS roomDescription,
+        r.localisation AS roomLocalisation,
+        r.capacity AS roomCapacity,
         r.floorSize AS roomFloorSize,
         IF(b.image_id is not null, CONCAT('https://api.opentalent.fr/app.php/_internal/secure/files/', b.image_id, '/raw'), null) AS imageUrl,
         IF(b.image_id is not null, CONCAT('https://api.opentalent.fr/app.php/_internal/secure/files/', b.image_id, '/300x0'), null) AS thumbnailUrl,
@@ -32,8 +32,8 @@ CREATE OR REPLACE VIEW view_public_events AS
             LEFT JOIN Categories AS cs ON(cs.id = ec.categories_id)
             LEFT JOIN Familly AS f ON(f.id = cs.familly_id)
          WHERE ec.event_id = b.id
-        ) AS categories, 
-        'opentalent' as origin, 
+        ) AS categories,
+        'opentalent' as origin,
         b.id as entityId
     FROM Booking AS b
         INNER JOIN Organization o ON o.id = b.organization_id
@@ -41,44 +41,44 @@ CREATE OR REPLACE VIEW view_public_events AS
         LEFT JOIN Place AS p ON (p.id = b.place_id)
         LEFT JOIN AddressPostal AS ap ON (ap.id = p.addressPostal_id)
         LEFT JOIN Room AS r ON (r.id = b.room_id)
-    WHERE b.discr = 'event' 
-        AND b.datetimeEnd >= NOW() 
-        AND b.visibility = 'PUBLIC_VISIBILITY' 
+    WHERE b.discr = 'event'
+        AND b.datetimeEnd >= NOW()
+        AND b.visibility = 'PUBLIC_VISIBILITY'
         AND b.isCanceled = 0
         AND b.gender_id <> 7 
     UNION
     SELECT
-        aw.uuid, 
-        null AS organizationId, 
-        aw.name, 
-        aw.description, 
-        aw.deepLink AS url, 
-        aw.datetimeStart, 
-        aw.datetimeEnd, 
+        aw.uuid,
+        null AS organizationId,
+        aw.name,
+        aw.description,
+        aw.deepLink AS url,
+        aw.datetimeStart,
+        aw.datetimeEnd,
         NULL as gender,
-        aw.priceMini, 
-        aw.priceMaxi, 
+        aw.priceMini,
+        aw.priceMaxi,
         aw.subCategory AS categoryCode,
-        aw.place AS placeName, 
-        NULL AS placeDescription, 
-        NULL AS placeFloorSize, 
+        aw.place AS placeName,
+        NULL AS placeDescription,
+        NULL AS placeFloorSize,
         NULL AS placeCapacity,
-        aw.city, 
-        aw.postalCode, 
-        aw.streetAddress, 
-        aw.longitude, 
+        aw.city,
+        aw.postalCode,
+        aw.streetAddress,
+        aw.longitude,
         aw.latitude,
-        NULL AS roomName, 
-        NULL AS roomDescription, 
-        NULL AS roomLocalisation, 
-        NULL AS roomCapacity, 
+        NULL AS roomName,
+        NULL AS roomDescription,
+        NULL AS roomLocalisation,
+        NULL AS roomCapacity,
         NULL AS roomFloorSize,
-        aw.largeimage AS imageUrl, 
-        aw.mediumimage as thumbnailUrl, 
-        aw.categoryCode AS categories, 
-        'awin' as origin, 
+        aw.largeimage AS imageUrl,
+        aw.mediumimage as thumbnailUrl,
+        aw.categoryCode AS categories,
+        'awin' as origin,
         aw.id as entityId
     FROM AwinProduct as aw
     WHERE
-        aw.datetimeEnd >= NOW() 
+        aw.datetimeEnd >= NOW()
         AND aw.datetimeStart IS NOT NULL;

+ 1 - 1
src/Commands/CronCommand.php

@@ -134,7 +134,7 @@ class CronCommand extends Command
             foreach (explode(',', $jobNames) as $name) {
                 try {
                     $jobs[] = $this->cronjobIterator->getByName($name);
-                } catch (RuntimeException $e) {
+                } catch (\RuntimeException $e) {
                     $this->output->writeln($e->getMessage());
                     $this->listJobs();
 

+ 0 - 0
src/Commands/DolibarrSyncCommand.php


+ 15 - 15
src/Entity/Access/Access.php

@@ -42,7 +42,6 @@ use App\Entity\Organization\OrganizationLicence;
 use App\Entity\Organization\Parameters;
 use App\Entity\Person\CommissionMember;
 use App\Entity\Person\CompanyPerson;
-use App\Entity\Person\Medal;
 use App\Entity\Person\Person;
 use App\Entity\Person\PersonActivity;
 use App\Entity\Place\PlaceRepair;
@@ -50,6 +49,7 @@ use App\Entity\Place\RoomRepair;
 use App\Entity\Product\Equipment;
 use App\Entity\Product\EquipmentLoan;
 use App\Entity\Product\EquipmentRepair;
+use App\Entity\Reward\AccessReward;
 use App\Filter\ApiPlatform\Person\FullNameFilter;
 use App\Filter\ApiPlatform\Utils\InFilter;
 use App\Repository\Access\AccessRepository;
@@ -282,8 +282,8 @@ class Access implements UserInterface, PasswordAuthenticatedUserInterface
     #[ORM\OneToMany(mappedBy: 'access', targetEntity: Donor::class, cascade: ['persist'], orphanRemoval: true)]
     private Collection $donors;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: Medal::class, cascade: ['persist'], orphanRemoval: true)]
-    private Collection $medals;
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: AccessReward::class, cascade: ['persist'], orphanRemoval: true)]
+    private Collection $accessRewards;
 
     #[ORM\OneToMany(mappedBy: 'access', targetEntity: OrganizationResponsability::class, cascade: ['persist'], orphanRemoval: true)]
     private Collection $organizationResponsabilities;
@@ -346,7 +346,7 @@ class Access implements UserInterface, PasswordAuthenticatedUserInterface
         $this->equipments = new ArrayCollection();
         $this->accessFictionalIntangibles = new ArrayCollection();
         $this->donors = new ArrayCollection();
-        $this->medals = new ArrayCollection();
+        $this->accessRewards = new ArrayCollection();
         $this->organizationResponsabilities = new ArrayCollection();
         $this->accessWishes = new ArrayCollection();
         $this->workByUsers = new ArrayCollection();
@@ -1832,29 +1832,29 @@ class Access implements UserInterface, PasswordAuthenticatedUserInterface
     }
 
     /**
-     * @return Collection<int, Medal>
+     * @return Collection<int, AccessReward>
      */
-    public function getMedals(): Collection
+    public function getAccessRewards(): Collection
     {
-        return $this->medals;
+        return $this->accessRewards;
     }
 
-    public function addMedal(Medal $medal): self
+    public function addAccessReward(AccessReward $accessRewards): self
     {
-        if (!$this->medals->contains($medal)) {
-            $this->medals[] = $medal;
-            $medal->setAccess($this);
+        if (!$this->accessRewards->contains($accessRewards)) {
+            $this->accessRewards[] = $accessRewards;
+            $accessRewards->setAccess($this);
         }
 
         return $this;
     }
 
-    public function removeMedal(Medal $medal): self
+    public function removeAccessReward(AccessReward $accessRewards): self
     {
-        if ($this->medals->removeElement($medal)) {
+        if ($this->accessRewards->removeElement($accessRewards)) {
             // set the owning side to null (unless already changed)
-            if ($medal->getAccess() === $this) {
-                $medal->setAccess(null);
+            if ($accessRewards->getAccess() === $this) {
+                $accessRewards->setAccess(null);
             }
         }
 

+ 32 - 0
src/Entity/Organization/Organization.php

@@ -34,6 +34,7 @@ use App\Entity\Person\Commission;
 use App\Entity\Place\Place;
 use App\Entity\Product\Equipment;
 use App\Entity\Product\Intangible;
+use App\Entity\Reward\Reward;
 use App\Enum\Organization\CategoryEnum;
 use App\Enum\Organization\LegalEnum;
 use App\Enum\Organization\OpcaEnum;
@@ -310,6 +311,9 @@ class Organization
     #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Donor::class, orphanRemoval: true)]
     private Collection $donors;
 
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Reward::class, orphanRemoval: true)]
+    private Collection $rewards;
+
     //    #[ORM\OneToOne()]
     //    private OnlineRegistrationSettings $onlineRegistrationSettings;
 
@@ -357,6 +361,7 @@ class Organization
         $this->intangibles = new ArrayCollection();
         $this->donors = new ArrayCollection();
         $this->tags = new ArrayCollection();
+        $this->rewards = new ArrayCollection();
     }
 
     public function getId(): ?int
@@ -1823,4 +1828,31 @@ class Organization
 
         return $this;
     }
+
+    public function getRewards(): Collection
+    {
+        return $this->rewards;
+    }
+
+    public function addReward(Reward $rewards): self
+    {
+        if (!$this->rewards->contains($rewards)) {
+            $this->rewards[] = $rewards;
+            $rewards->setOrganization($this);
+        }
+
+        return $this;
+    }
+
+    public function removeReward(Reward $rewards): self
+    {
+        if ($this->rewards->removeElement($rewards)) {
+            // set the owning side to null (unless already changed)
+            if ($rewards->getOrganization() === $this) {
+                $rewards->setOrganization(null);
+            }
+        }
+
+        return $this;
+    }
 }

+ 3 - 3
src/Entity/Person/Medal.php → src/Entity/Reward/AccessReward.php

@@ -2,7 +2,7 @@
 
 declare(strict_types=1);
 
-namespace App\Entity\Person;
+namespace App\Entity\Reward;
 
 use App\Entity\Access\Access;
 // use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Auditable;
@@ -13,14 +13,14 @@ use Doctrine\ORM\Mapping as ORM;
  */
 // #[Auditable]
 #[ORM\Entity]
-class Medal
+class AccessReward
 {
     #[ORM\Id]
     #[ORM\Column]
     #[ORM\GeneratedValue]
     private ?int $id = null;
 
-    #[ORM\ManyToOne(inversedBy: 'medals')]
+    #[ORM\ManyToOne(inversedBy: 'accessRewards')]
     #[ORM\JoinColumn(nullable: false)]
     private Access $access;
 

+ 43 - 0
src/Entity/Reward/Reward.php

@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Entity\Reward;
+
+// use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Auditable;
+use App\Entity\Organization\Organization;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * Classe qui gère les disctinctions de la CMF.
+ */
+// #[Auditable]
+#[ORM\Entity]
+class Reward
+{
+    #[ORM\Id]
+    #[ORM\Column]
+    #[ORM\GeneratedValue]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'rewards')]
+    #[ORM\JoinColumn(nullable: false)]
+    private Organization $organization;
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getOrganization(): ?Organization
+    {
+        return $this->organization;
+    }
+
+    public function setOrganization(?Organization $organization): self
+    {
+        $this->organization = $organization;
+
+        return $this;
+    }
+}

+ 66 - 0
src/Filter/ApiPlatform/Utils/FindInSetFilter.php~2.4.1

@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Filter\ApiPlatform\Utils;
+
+use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
+use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use ApiPlatform\Metadata\Operation;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\PropertyInfo\Type;
+
+final class FindInSetFilter extends AbstractFilter
+{
+    /**
+     * @param mixed[] $context
+     */
+    protected function filterProperty(string $property,
+        mixed $value,
+        QueryBuilder $queryBuilder,
+        QueryNameGeneratorInterface $queryNameGenerator,
+        string $resourceClass,
+        ?Operation $operation = null,
+        array $context = []): void
+    {
+        // otherwise filter is applied to order and page as well
+        if (!$this->isPropertyEnabled($property, $resourceClass) || !$this->isPropertyMapped($property, $resourceClass)
+        ) {
+            return;
+        }
+
+        $alias = $queryBuilder->getRootAliases()[0];
+        $parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
+        $queryBuilder
+            ->andWhere(sprintf('find_in_set(:%s, %s.%s) <> 0', $parameterName, $alias, $property))
+            ->setParameter($parameterName, explode(',', $value));
+    }
+
+    /**
+     * API docs.
+     *
+     * @return array<string, mixed[]>
+     */
+    public function getDescription(string $resourceClass): array
+    {
+        if (!$this->properties) {
+            return [];
+        }
+
+        $description = [];
+        foreach ($this->properties as $property => $strategy) {
+            $description["$property"] = [
+                'property' => $property,
+                'type' => Type::BUILTIN_TYPE_STRING,
+                'required' => false,
+                'swagger' => [
+                    'description' => "Filtre de type find_in_set(), vérifie que la valeur est dans le set d'un champs de type CSV",
+                    'name' => 'FindInSet Filter',
+                    'type' => 'Utils Filter',
+                ],
+            ];
+        }
+
+        return $description;
+    }
+}

+ 66 - 0
src/Filter/ApiPlatform/Utils/FindInSetFilter.php~HEAD

@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Filter\ApiPlatform\Utils;
+
+use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
+use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use ApiPlatform\Metadata\Operation;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\PropertyInfo\Type;
+
+final class FindInSetFilter extends AbstractFilter
+{
+    /**
+     * @param mixed[] $context
+     */
+    protected function filterProperty(string $property,
+        mixed $value,
+        QueryBuilder $queryBuilder,
+        QueryNameGeneratorInterface $queryNameGenerator,
+        string $resourceClass,
+        ?Operation $operation = null,
+        array $context = []): void
+    {
+        // otherwise filter is applied to order and page as well
+        if (!$this->isPropertyEnabled($property, $resourceClass) || !$this->isPropertyMapped($property, $resourceClass)
+        ) {
+            return;
+        }
+
+        $alias = $queryBuilder->getRootAliases()[0];
+        $parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
+        $queryBuilder
+            ->andWhere(sprintf('find_in_set(:%s, %s.%s) <> 0', $parameterName, $alias, $property))
+            ->setParameter($parameterName, explode(',', $value));
+    }
+
+    /**
+     * API docs.
+     *
+     * @return array<string, mixed[]>
+     */
+    public function getDescription(string $resourceClass): array
+    {
+        if (!$this->properties) {
+            return [];
+        }
+
+        $description = [];
+        foreach ($this->properties as $property => $strategy) {
+            $description["$property"] = [
+                'property' => $property,
+                'type' => Type::BUILTIN_TYPE_STRING,
+                'required' => false,
+                'swagger' => [
+                    'description' => "Filtre de type find_in_set(), vérifie que la valeur est dans le set d'un champs de type CSV",
+                    'name' => 'FindInSet Filter',
+                    'type' => 'Utils Filter',
+                ],
+            ];
+        }
+
+        return $description;
+    }
+}

+ 78 - 0
src/Filter/ApiPlatform/Utils/InFilter.php~2.4.1

@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Filter\ApiPlatform\Utils;
+
+use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
+use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use ApiPlatform\Metadata\Operation;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\PropertyInfo\Type;
+
+/**
+ * Is property included in the given CSV array.
+ *
+ * Ex:
+ *
+ *    /api/accesses?id[in]=123,124,125
+ */
+final class InFilter extends AbstractFilter
+{
+    /**
+     * @param mixed[] $context
+     */
+    protected function filterProperty(string $property,
+        mixed $value,
+        QueryBuilder $queryBuilder,
+        QueryNameGeneratorInterface $queryNameGenerator,
+        string $resourceClass,
+        ?Operation $operation = null,
+        array $context = []): void
+    {
+        // otherwise filter is applied to order and page as well
+        if (
+            !$this->isPropertyEnabled($property, $resourceClass)
+            || !$this->isPropertyMapped($property, $resourceClass)
+        ) {
+            return;
+        }
+
+        if (!isset($value['in'])) {
+            return;
+        }
+
+        $alias = $queryBuilder->getRootAliases()[0];
+        $parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
+        $queryBuilder
+            ->andWhere(sprintf('%s.%s IN (%s)', $alias, $property, $value['in']));
+    }
+
+    /**
+     * API docs.
+     *
+     * @return array<string, mixed[]>
+     */
+    public function getDescription(string $resourceClass): array
+    {
+        if (!$this->properties) {
+            return [];
+        }
+
+        $description = [];
+        foreach ($this->properties as $property => $strategy) {
+            $description[$property.'[in]'] = [
+                'property' => $property,
+                'type' => Type::BUILTIN_TYPE_STRING,
+                'required' => false,
+                'swagger' => [
+                    'description' => 'Filtre permettant d\'utiliser les IN. (usage: `id[in]=1,2,3`)',
+                    'name' => 'In Filter',
+                    'type' => 'Utils Filter',
+                ],
+            ];
+        }
+
+        return $description;
+    }
+}

+ 78 - 0
src/Filter/ApiPlatform/Utils/InFilter.php~HEAD

@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Filter\ApiPlatform\Utils;
+
+use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
+use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use ApiPlatform\Metadata\Operation;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\PropertyInfo\Type;
+
+/**
+ * Is property included in the given CSV array.
+ *
+ * Ex:
+ *
+ *    /api/accesses?id[in]=123,124,125
+ */
+final class InFilter extends AbstractFilter
+{
+    /**
+     * @param mixed[] $context
+     */
+    protected function filterProperty(string $property,
+        mixed $value,
+        QueryBuilder $queryBuilder,
+        QueryNameGeneratorInterface $queryNameGenerator,
+        string $resourceClass,
+        ?Operation $operation = null,
+        array $context = []): void
+    {
+        // otherwise filter is applied to order and page as well
+        if (
+            !$this->isPropertyEnabled($property, $resourceClass)
+            || !$this->isPropertyMapped($property, $resourceClass)
+        ) {
+            return;
+        }
+
+        if (!isset($value['in'])) {
+            return;
+        }
+
+        $alias = $queryBuilder->getRootAliases()[0];
+        $parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
+        $queryBuilder
+            ->andWhere(sprintf('%s.%s IN (%s)', $alias, $property, $value['in']));
+    }
+
+    /**
+     * API docs.
+     *
+     * @return array<string, mixed[]>
+     */
+    public function getDescription(string $resourceClass): array
+    {
+        if (!$this->properties) {
+            return [];
+        }
+
+        $description = [];
+        foreach ($this->properties as $property => $strategy) {
+            $description[$property.'[in]'] = [
+                'property' => $property,
+                'type' => Type::BUILTIN_TYPE_STRING,
+                'required' => false,
+                'swagger' => [
+                    'description' => 'Filtre permettant d\'utiliser les IN. (usage: `id[in]=1,2,3`)',
+                    'name' => 'In Filter',
+                    'type' => 'Utils Filter',
+                ],
+            ];
+        }
+
+        return $description;
+    }
+}

+ 11 - 0
src/Filter/Doctrine/TimeConstraint/TimeConstraintInterface.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace App\Filter\Doctrine\TimeConstraint;
+
+interface TimeConstraintInterface
+{
+    /**
+     * @return array<string, array<string, list<int>>>
+     */
+    public function invoke(int $accessId): array;
+}

+ 8 - 8
src/Service/ApiResourceBuilder/Dolibarr/DolibarrAccountBuilder.php

@@ -13,11 +13,11 @@ use App\Service\Dolibarr\DolibarrApiService;
 class DolibarrAccountBuilder
 {
     public const PRODUCT_MAPPING = [
-        1 => 'PRODUCT_ARTIST',   // OT Artist
-        2 => 'PRODUCT_ARTIST_PREMIUM',   // OT Artist Premium
-        3 => 'PRODUCT_SCHOOL',   // OT School Standard
-        4 => 'PRODUCT_SCHOOL_PREMIUM',   // OT School Premium
-        5 => 'PRODUCT_MANAGER',   // OT Manager
+        'Opentalent Artist' => 'PRODUCT_ARTIST',   // OT Artist
+        'Opentalent Artist Premium' => 'PRODUCT_ARTIST_PREMIUM',   // OT Artist Premium
+        'Opentalent School' => 'PRODUCT_SCHOOL',   // OT School Standard
+        'Opentalent School Premium' => 'PRODUCT_SCHOOL_PREMIUM',   // OT School Premium
+        'Opentalent Manager' => 'PRODUCT_MANAGER',   // OT Manager
     ];
 
     public function __construct(
@@ -68,11 +68,11 @@ class DolibarrAccountBuilder
                         ->setClientNumber($accountData['code_client']);
 
         if (
-            array_key_exists('options_2iopen_software_used', $accountData['array_options'])
-            && !empty($accountData['array_options']['options_2iopen_software_used'])
+            array_key_exists('2iopen_software_opentalent', $accountData['array_options'])
+            && !empty($accountData['array_options']['2iopen_software_opentalent'])
         ) {
             $dolibarrAccount->setProduct(
-                self::PRODUCT_MAPPING[(int) $accountData['array_options']['options_2iopen_software_used']]
+                self::PRODUCT_MAPPING[$accountData['array_options']['2iopen_software_opentalent']]
             );
         }
 

+ 0 - 1
src/Service/Cotisation/Utils.php

@@ -11,7 +11,6 @@ use App\Repository\Network\NetworkOrganizationRepository;
 use App\Service\Network\Utils as NetworkUtils;
 use App\Service\Organization\Utils as OrganizationUtils;
 use App\Service\Utils\DatesUtils;
-use App\Tests\Service\Cotisation\UtilsTest;
 
 /**
  * Class Utils : Service rassemblant des fonctions d'interrogation pour gérer des conditions dans les Cotisations.

+ 66 - 5
src/Service/Organization/Utils.php

@@ -7,9 +7,10 @@ namespace App\Service\Organization;
 use App\Entity\Organization\Organization;
 use App\Enum\Organization\OrganizationIdsEnum;
 use App\Enum\Organization\SettingsProductEnum;
+use App\Repository\Network\NetworkOrganizationRepository;
+use App\Service\Network\Utils as NetworkUtils;
 use App\Service\Utils\DatesUtils;
 use App\Service\Utils\UrlBuilder;
-use App\Test\Service\Organization\UtilsTest;
 
 /**
  * Service rassemblant des fonctions d'aide pour les questions se rapportant à l'organisation.
@@ -18,6 +19,13 @@ class Utils
 {
     public const START_DATE_KEY = 'dateStart';
     public const END_DATE_KEY = 'dateEnd';
+    public const OUT_OF_NET_SUBDOMAIN = 'outofnet';
+
+    public function __construct(
+        private NetworkOrganizationRepository $networkOrganizationRepository,
+        private NetworkUtils $networkUtils
+    ) {
+    }
 
     /**
      * Teste si l'organisation est considérée comme une structure == n'a pas un produit manager.
@@ -75,17 +83,67 @@ class Utils
     /**
      * Test si l'organisation est la structure CMF.
      *
-     * @see UtilsTest::testIsOrganizationIsCMF()
+     * @see UtilsTest::testIsCMF()
      */
-    public function isOrganizationCMF(Organization $organization): bool
+    public function isCMF(Organization $organization): bool
     {
         return $this->isOrganizationIdEqualTo($organization, OrganizationIdsEnum::CMF);
     }
 
     /**
-     * Test si l'id de l'organisation est celui passé en paramètre (doit faire partit des OrganizationIdsEnum).
+     * Test si l'organisation est un dernier parent ET appartient à la CMF.
+     *
+     * @see UtilsTest::testIsLastParentAndCMF()
+     */
+    public function isLastParentAndCMF(Organization $organization): bool
+    {
+        return $this->networkOrganizationRepository->isLastParent($organization) && $this->networkUtils->isCMF($organization);
+    }
+
+    /**
+     * Test si l'organisation est une structure (non manager) ET appartient à la CMF.
+     *
+     * @see UtilsTest::testIsStructureAndCMF()
      */
-    protected function isOrganizationIdEqualTo(Organization $organization, OrganizationIdsEnum $organizationIds): bool
+    public function isStructureAndCMF(Organization $organization): bool
+    {
+        return $this->isStructure($organization) && $this->networkUtils->isCMF($organization);
+    }
+
+    /**
+     * Test si la structure est un manager ET qu'elle appartient à la CMF.
+     *
+     * @see UtilsTest::testIsManagerAndCMF()
+     */
+    public function isManagerAndCMF(Organization $organization): bool
+    {
+        return $this->isManager($organization) && $this->networkUtils->isCMF($organization);
+    }
+
+    /**
+     * Test si l'organisation est un manager ET un dernier parent ET appartient à la CMF.
+     *
+     * @see UtilsTest::testIsManagerAndLastParentAndCMF()
+     */
+    public function isManagerAndLastParentAndCMF(Organization $organization): bool
+    {
+        return $this->isManager($organization) && $this->isLastParentAndCMF($organization);
+    }
+
+    /**
+     * Test si l'organisation est un manager ET n'est pas un dernier parent ET appartient à la CMF.
+     *
+     * @see UtilsTest::testIsManagerAndNotLastParentAndCMF()
+     */
+    public function isManagerAndNotLastParentAndCMF(Organization $organization): bool
+    {
+        return $this->isManager($organization) && !$this->networkOrganizationRepository->isLastParent($organization) && $this->networkUtils->isCMF($organization);
+    }
+
+    /**
+     * Test si l'id de l'organisation est celui passé en paramètre (doit faire partie des OrganizationIdsEnum).
+     */
+    public function isOrganizationIdEqualTo(Organization $organization, OrganizationIdsEnum $organizationIds): bool
     {
         return $organization->getId() === $organizationIds->value;
     }
@@ -211,6 +269,9 @@ class Utils
         if (!$subdomain) {
             return null;
         }
+        if ($subdomain === self::OUT_OF_NET_SUBDOMAIN) {
+            return 'https://opentalent.fr';
+        }
 
         return 'https://'.$subdomain.'.opentalent.fr';
     }

+ 0 - 3
src/Service/Rest/Operation/CreateOperation.php

@@ -4,8 +4,6 @@ declare(strict_types=1);
 
 namespace App\Service\Rest\Operation;
 
-use JetBrains\PhpStorm\Pure;
-
 /**
  * A single create operation (a POST request).
  */
@@ -18,7 +16,6 @@ class CreateOperation extends BaseRestOperation
      * @param array<mixed> $parameters
      * @param array<mixed> $options
      */
-    #[Pure]
     public function __construct(
         protected string $label,
         protected string $entityName,

+ 10 - 219
tests/Unit/Service/Cotisation/UtilsTest.php

@@ -27,220 +27,23 @@ class UtilsTest extends TestCase
     public function setUp(): void
     {
         $this->networkOrganizationRepository = $this->getMockBuilder(NetworkOrganizationRepository::class)
-                ->disableOriginalConstructor()
-                ->getMock();
+            ->disableOriginalConstructor()
+            ->getMock();
 
         $this->networkUtils = $this->getMockBuilder(NetworkUtils::class)->disableOriginalConstructor()->getMock();
-        $this->organizationUtils = $this->getMockBuilder(OrganizationUtils::class)->getMock();
+        $this->organizationUtils = $this->getMockBuilder(OrganizationUtils::class)->disableOriginalConstructor()->getMock();
 
         $this->cotisationApiResourcesRepository = $this->getMockBuilder(CotisationApiResourcesRepository::class)
             ->disableOriginalConstructor()
             ->getMock();
     }
 
-    /**
-     * @see Utils::isLastParentAndCMF()
-     */
-    public function testIsLastParentAndCMF(): void
+    public function getCotisationUtilsMockFor(string $methodName): MockObject|CotisationUtils
     {
-        $cotisationUtils = $this->getMockBuilder(CotisationUtils::class)
+        return $this->getMockBuilder(CotisationUtils::class)
             ->setConstructorArgs([$this->networkUtils, $this->organizationUtils, $this->networkOrganizationRepository, $this->cotisationApiResourcesRepository])
-            ->setMethodsExcept(['isLastParentAndCMF'])
+            ->setMethodsExcept([$methodName])
             ->getMock();
-
-        $organization1 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization2 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization3 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization4 = $this->getMockBuilder(Organization::class)->getMock();
-
-        $this->networkOrganizationRepository
-                ->method('isLastParent')
-                ->willReturnMap([
-                    [$organization1, true],
-                    [$organization2, true],
-                    [$organization3, false],
-                    [$organization4, false],
-                ]);
-
-        $this->networkUtils
-                ->method('isCMF')
-                ->willReturnMap([
-                    [$organization1, true],
-                    [$organization2, false],
-                    [$organization3, true],
-                    [$organization4, false],
-                ]);
-
-        $this->assertTrue($cotisationUtils->isLastParentAndCMF($organization1));
-        $this->assertFalse($cotisationUtils->isLastParentAndCMF($organization2));
-        $this->assertFalse($cotisationUtils->isLastParentAndCMF($organization3));
-        $this->assertFalse($cotisationUtils->isLastParentAndCMF($organization4));
-    }
-
-    /**
-     * @see Utils::isStructureAndCMF()
-     */
-    public function testIsStructureAndCMF(): void
-    {
-        $cotisationUtils = $this->getMockBuilder(CotisationUtils::class)
-            ->setConstructorArgs([$this->networkUtils, $this->organizationUtils, $this->networkOrganizationRepository, $this->cotisationApiResourcesRepository])
-            ->setMethodsExcept(['isStructureAndCMF'])
-            ->getMock();
-
-        $organization1 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization2 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization3 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization4 = $this->getMockBuilder(Organization::class)->getMock();
-
-        $this->organizationUtils
-            ->method('isStructure')
-            ->willReturnMap([
-                [$organization1, true],
-                [$organization2, false],
-                [$organization3, true],
-                [$organization4, false],
-            ]);
-
-        $this->networkUtils
-            ->method('isCMF')
-            ->with($organization1)
-            ->willReturnMap([
-                [$organization1, true],
-                [$organization2, false],
-                [$organization3, false],
-                [$organization4, true],
-            ]);
-
-        $this->assertTrue($cotisationUtils->isStructureAndCMF($organization1));
-        $this->assertFalse($cotisationUtils->isStructureAndCMF($organization2));
-        $this->assertFalse($cotisationUtils->isStructureAndCMF($organization3));
-        $this->assertFalse($cotisationUtils->isStructureAndCMF($organization4));
-    }
-
-    /**
-     * @see Utils::isManagerAndCMF()
-     */
-    public function testIsManagerAndCMF(): void
-    {
-        $cotisationUtils = $this->getMockBuilder(CotisationUtils::class)
-            ->setConstructorArgs([$this->networkUtils, $this->organizationUtils, $this->networkOrganizationRepository, $this->cotisationApiResourcesRepository])
-            ->setMethodsExcept(['isManagerAndCMF'])
-            ->getMock();
-
-        $organization1 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization2 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization3 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization4 = $this->getMockBuilder(Organization::class)->getMock();
-
-        $this->organizationUtils
-            ->method('isManager')
-            ->willReturnMap([
-                [$organization1, true],
-                [$organization2, false],
-                [$organization3, true],
-                [$organization4, false],
-            ]);
-
-        $this->networkUtils
-            ->method('isCMF')
-            ->willReturnMap([
-                [$organization1, true],
-                [$organization2, false],
-                [$organization3, false],
-                [$organization4, true],
-            ]);
-
-        $this->assertTrue($cotisationUtils->isManagerAndCMF($organization1));
-        $this->assertFalse($cotisationUtils->isManagerAndCMF($organization2));
-        $this->assertFalse($cotisationUtils->isManagerAndCMF($organization3));
-        $this->assertFalse($cotisationUtils->isManagerAndCMF($organization4));
-    }
-
-    /**
-     * @see Utils::isManagerAndNotLastParentAndCMF()
-     */
-    public function testIsManagerAndLastParentAndCMF(): void
-    {
-        $cotisationUtils = $this->getMockBuilder(CotisationUtils::class)
-            ->setConstructorArgs([$this->networkUtils, $this->organizationUtils, $this->networkOrganizationRepository, $this->cotisationApiResourcesRepository])
-            ->setMethodsExcept(['isManagerAndLastParentAndCMF'])
-            ->getMock();
-
-        $organization1 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization2 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization3 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization4 = $this->getMockBuilder(Organization::class)->getMock();
-
-        $this->organizationUtils
-            ->method('isManager')
-            ->willReturnMap([
-                [$organization1, true],
-                [$organization2, false],
-                [$organization3, true],
-                [$organization4, false],
-            ]);
-
-        $cotisationUtils
-            ->method('isLastParentAndCMF')
-            ->willReturnMap([
-                [$organization1, true],
-                [$organization2, false],
-                [$organization3, false],
-                [$organization4, true],
-            ]);
-
-        $this->assertTrue($cotisationUtils->isManagerAndLastParentAndCMF($organization1));
-        $this->assertFalse($cotisationUtils->isManagerAndLastParentAndCMF($organization2));
-        $this->assertFalse($cotisationUtils->isManagerAndLastParentAndCMF($organization3));
-        $this->assertFalse($cotisationUtils->isManagerAndLastParentAndCMF($organization4));
-    }
-
-    /**
-     * @see Utils::isManagerAndNotLastParentAndCMF()
-     */
-    public function testIsManagerAndNotLastParentAndCMF(): void
-    {
-        $cotisationUtils = $this->getMockBuilder(CotisationUtils::class)
-            ->setConstructorArgs([$this->networkUtils, $this->organizationUtils, $this->networkOrganizationRepository, $this->cotisationApiResourcesRepository])
-            ->setMethodsExcept(['isManagerAndNotLastParentAndCMF'])
-            ->getMock();
-
-        $organization1 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization2 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization3 = $this->getMockBuilder(Organization::class)->getMock();
-        $organization4 = $this->getMockBuilder(Organization::class)->getMock();
-
-        $this->organizationUtils
-            ->method('isManager')
-            ->willReturnMap([
-                [$organization1, true],
-                [$organization2, false],
-                [$organization3, true],
-                [$organization4, true],
-            ]);
-
-        $this->networkOrganizationRepository
-            ->method('isLastParent')
-            ->willReturnMap([
-                [$organization1, false],
-                [$organization2, false],
-                [$organization3, true],
-                [$organization4, false],
-            ]);
-
-        $this->networkUtils
-            ->method('isCMF')
-            ->willReturnMap([
-                [$organization1, true],
-                [$organization2, true],
-                [$organization3, true],
-                [$organization4, false],
-            ]);
-
-        $this->assertTrue($cotisationUtils->isManagerAndNotLastParentAndCMF($organization1));
-        $this->assertFalse($cotisationUtils->isManagerAndNotLastParentAndCMF($organization2));
-        $this->assertFalse($cotisationUtils->isManagerAndNotLastParentAndCMF($organization3));
-        $this->assertFalse($cotisationUtils->isManagerAndNotLastParentAndCMF($organization4));
     }
 
     /**
@@ -248,10 +51,7 @@ class UtilsTest extends TestCase
      */
     public function testGetAlertState(): void
     {
-        $cotisationUtils = $this->getMockBuilder(CotisationUtils::class)
-            ->setConstructorArgs([$this->networkUtils, $this->organizationUtils, $this->networkOrganizationRepository, $this->cotisationApiResourcesRepository])
-            ->setMethodsExcept(['getAlertState'])
-            ->getMock();
+        $cotisationUtils = $this->getCotisationUtilsMockFor('getAlertState');
 
         $organization = $this->getMockBuilder(Organization::class)->getMock();
         $organization->method('getId')->willReturn(1);
@@ -275,10 +75,7 @@ class UtilsTest extends TestCase
      */
     public function testGetAlertStateInsurance(): void
     {
-        $cotisationUtils = $this->getMockBuilder(CotisationUtils::class)
-            ->setConstructorArgs([$this->networkUtils, $this->organizationUtils, $this->networkOrganizationRepository, $this->cotisationApiResourcesRepository])
-            ->setMethodsExcept(['getAlertState'])
-            ->getMock();
+        $cotisationUtils = $this->getCotisationUtilsMockFor('getAlertState');
 
         $year = 2022;
 
@@ -303,10 +100,7 @@ class UtilsTest extends TestCase
      */
     public function testGetAlertStateAdvertisingInsurance(): void
     {
-        $cotisationUtils = $this->getMockBuilder(CotisationUtils::class)
-            ->setConstructorArgs([$this->networkUtils, $this->organizationUtils, $this->networkOrganizationRepository, $this->cotisationApiResourcesRepository])
-            ->setMethodsExcept(['getAlertState'])
-            ->getMock();
+        $cotisationUtils = $this->getCotisationUtilsMockFor('getAlertState');
 
         $year = 2022;
 
@@ -331,10 +125,7 @@ class UtilsTest extends TestCase
      */
     public function testGetCurrentCotisationYear(): void
     {
-        $cotisationUtils = $this->getMockBuilder(CotisationUtils::class)
-            ->setConstructorArgs([$this->networkUtils, $this->organizationUtils, $this->networkOrganizationRepository, $this->cotisationApiResourcesRepository])
-            ->setMethodsExcept(['getCurrentCotisationYear'])
-            ->getMock();
+        $cotisationUtils = $this->getCotisationUtilsMockFor('getCurrentCotisationYear');
 
         DatesUtils::setFakeDatetime('2022-03-01');
 

+ 5 - 1
tests/Unit/Service/Dolibarr/DolibarrAccountCreatorTest.php

@@ -115,7 +115,11 @@ class DolibarrAccountCreatorTest extends TestCase
             ->getMock();
 
         $organizationId = 1;
-        $accountData = ['id' => '2', 'code_client' => 'C2', 'array_options' => ['options_2iopen_software_used' => 1]];
+        $accountData = [
+            'id' => '2',
+            'code_client' => 'C2',
+            'array_options' => ['2iopen_software_opentalent' => 'Opentalent Artist'],
+        ];
 
         $dolibarrAccount = $dolibarrAccountCreator->createDolibarrAccount($organizationId, $accountData);
 

+ 264 - 40
tests/Unit/Service/Organization/UtilsTest.php

@@ -10,27 +10,53 @@ use App\Entity\Organization\Settings;
 use App\Entity\Organization\Subdomain;
 use App\Enum\Organization\OrganizationIdsEnum;
 use App\Enum\Organization\SettingsProductEnum;
+use App\Repository\Network\NetworkOrganizationRepository;
+use App\Service\Network\Utils as NetworkUtils;
 use App\Service\Organization\Utils as OrganizationUtils;
 use App\Service\Utils\DatesUtils;
 use Doctrine\Common\Collections\ArrayCollection;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 
-class TestableOrganizationUtils extends OrganizationUtils
+class UtilsTest extends TestCase
 {
-    public function isOrganizationIdEqualTo(Organization $organization, OrganizationIdsEnum $organizationIdsEnum): bool
+    private MockObject|NetworkOrganizationRepository $networkOrganizationRepository;
+    private MockObject|NetworkUtils $networkUtils;
+
+    public function setUp(): void
     {
-        return parent::isOrganizationIdEqualTo($organization, $organizationIdsEnum);
+        $this->networkOrganizationRepository = $this->getMockBuilder(NetworkOrganizationRepository::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->networkUtils = $this->getMockBuilder(NetworkUtils::class)->disableOriginalConstructor()->getMock();
+    }
+
+    public function getOrganizationUtilsMockFor(string $methodName): MockObject|OrganizationUtils
+    {
+        return $this->getMockBuilder(OrganizationUtils::class)
+            ->setConstructorArgs([$this->networkOrganizationRepository, $this->networkUtils])
+            ->setMethodsExcept([$methodName])
+            ->getMock();
+    }
+
+    private function createOrganizationWithProductMock(SettingsProductEnum $product): Organization
+    {
+        $settings = $this->getMockBuilder(Settings::class)->getMock();
+        $settings->method('getProduct')->willReturn($product);
+
+        $organization = $this->getMockBuilder(Organization::class)->getMock();
+        $organization->method('getSettings')->willReturn($settings);
+
+        return $organization;
     }
-}
 
-class UtilsTest extends TestCase
-{
     /**
      * @see OrganizationUtils::isStructure()
      */
     public function testIsStructure(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['isStructure'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('isStructure');
 
         $organization = $this->createOrganizationWithProductMock(SettingsProductEnum::ARTIST);
         $this->assertTrue($organizationUtils->isStructure($organization));
@@ -56,7 +82,7 @@ class UtilsTest extends TestCase
      */
     public function testIsManager(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['isManager'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('isManager');
 
         $organization = $this->createOrganizationWithProductMock(SettingsProductEnum::ARTIST);
         $this->assertFalse($organizationUtils->isManager($organization));
@@ -82,7 +108,7 @@ class UtilsTest extends TestCase
      */
     public function testIsOrganizationIs2ios(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['is2iosOrganization'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('is2iosOrganization');
 
         $organization = $this->getMockBuilder(Organization::class)->getMock();
 
@@ -92,17 +118,17 @@ class UtilsTest extends TestCase
     }
 
     /**
-     * @see OrganizationUtils::isOrganizationCMF()
+     * @see OrganizationUtils::isCMF()
      */
-    public function testIsOrganizationIsCMF(): void
+    public function testIsCMF(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['isOrganizationCMF'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('isCMF');
 
         $organization = $this->getMockBuilder(Organization::class)->getMock();
 
         $organizationUtils->expects(self::once())->method('isOrganizationIdEqualTo')->with($organization, OrganizationIdsEnum::CMF);
 
-        $organizationUtils->isOrganizationCMF($organization);
+        $organizationUtils->isCMF($organization);
     }
 
     /**
@@ -110,7 +136,7 @@ class UtilsTest extends TestCase
      */
     public function testIsOrganizationIdEqualTo(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['isOrganizationIdEqualTo'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('isOrganizationIdEqualTo');
 
         $organization = $this->getMockBuilder(Organization::class)->getMock();
         $organization->method('getId')->willReturnOnConsecutiveCalls(123, OrganizationIdsEnum::_2IOS->value);
@@ -124,7 +150,7 @@ class UtilsTest extends TestCase
      */
     public function testGetOrganizationCurrentActivityYear(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['getOrganizationCurrentActivityYear'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('getOrganizationCurrentActivityYear');
 
         $parameters = $this->getMockBuilder(Parameters::class)->getMock();
         $parameters->method('getMusicalDate')->willReturnOnConsecutiveCalls(
@@ -155,7 +181,7 @@ class UtilsTest extends TestCase
      */
     public function testGetActivityPeriodsSwitchYear(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['getActivityPeriodsSwitchYear'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('getActivityPeriodsSwitchYear');
 
         $parameters = $this->getMockBuilder(Parameters::class)->getMock();
         $parameters->method('getMusicalDate')->willReturn(new \DateTime('2020-09-05'));
@@ -174,7 +200,7 @@ class UtilsTest extends TestCase
      */
     public function testGetActivityPeriodsSwitchYearNoMusicalDate(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['getActivityPeriodsSwitchYear'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('getActivityPeriodsSwitchYear');
 
         $parameters = $this->getMockBuilder(Parameters::class)->getMock();
         $parameters->method('getMusicalDate')->willReturn(null);
@@ -193,7 +219,7 @@ class UtilsTest extends TestCase
      */
     public function testGetActivityYearSwitchDate(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['getActivityYearSwitchDate'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('getActivityYearSwitchDate');
 
         $parameters = $this->getMockBuilder(Parameters::class)->getMock();
         $parameters->method('getMusicalDate')->willReturn(new \DateTime('2020-09-05'));
@@ -210,7 +236,7 @@ class UtilsTest extends TestCase
      */
     public function testGetActivityYearSwitchDateNoMusicalDate(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['getActivityYearSwitchDate'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('getActivityYearSwitchDate');
 
         $parameters = $this->getMockBuilder(Parameters::class)->getMock();
         $parameters->method('getMusicalDate')->willReturn(null);
@@ -227,7 +253,7 @@ class UtilsTest extends TestCase
      */
     public function testGetOrganizationActiveSubdomain(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['getOrganizationActiveSubdomain'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('getOrganizationActiveSubdomain');
 
         $subdomain1 = $this->getMockBuilder(Subdomain::class)->getMock();
         $subdomain1->method('isActive')->willReturn(false);
@@ -250,7 +276,7 @@ class UtilsTest extends TestCase
      */
     public function testGetOrganizationActiveSubdomainNone(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['getOrganizationActiveSubdomain'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('getOrganizationActiveSubdomain');
 
         $subdomain1 = $this->getMockBuilder(Subdomain::class)->getMock();
         $subdomain1->method('isActive')->willReturn(false);
@@ -266,7 +292,7 @@ class UtilsTest extends TestCase
      */
     public function testOrganizationWebsite(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['getOrganizationWebsite'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('getOrganizationWebsite');
 
         $parameters = $this->getMockBuilder(Parameters::class)->getMock();
         $parameters->method('getDesactivateOpentalentSiteWeb')->willReturn(false);
@@ -285,7 +311,7 @@ class UtilsTest extends TestCase
      */
     public function testOrganizationWebsiteDeactivatedWithOther(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['getOrganizationWebsite'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('getOrganizationWebsite');
 
         $parameters = $this->getMockBuilder(Parameters::class)->getMock();
         $parameters->method('getDesactivateOpentalentSiteWeb')->willReturn(true);
@@ -302,7 +328,7 @@ class UtilsTest extends TestCase
      */
     public function testOrganizationWebsiteDeactivatedNoOther(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['getOrganizationWebsite'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('getOrganizationWebsite');
 
         $parameters = $this->getMockBuilder(Parameters::class)->getMock();
         $parameters->method('getDesactivateOpentalentSiteWeb')->willReturn(true);
@@ -319,7 +345,7 @@ class UtilsTest extends TestCase
      */
     public function testOrganizationWebsiteWithCustom(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['getOrganizationWebsite'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('getOrganizationWebsite');
 
         $parameters = $this->getMockBuilder(Parameters::class)->getMock();
         $parameters->method('getDesactivateOpentalentSiteWeb')->willReturn(false);
@@ -336,7 +362,7 @@ class UtilsTest extends TestCase
      */
     public function testOrganizationWebsiteNoSubdomains(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['getOrganizationWebsite'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('getOrganizationWebsite');
 
         $parameters = $this->getMockBuilder(Parameters::class)->getMock();
         $parameters->method('getDesactivateOpentalentSiteWeb')->willReturn(false);
@@ -350,12 +376,221 @@ class UtilsTest extends TestCase
         $this->assertEquals(null, $organizationUtils->getOrganizationWebsite($organization));
     }
 
+    /**
+     * @see OrganizationUtils::getOrganizationWebsite()
+     */
+    public function testOrganizationWebsiteOutOfNetSubdomain(): void
+    {
+        $organizationUtils = $this->getOrganizationUtilsMockFor('getOrganizationWebsite');
+
+        $parameters = $this->getMockBuilder(Parameters::class)->getMock();
+        $parameters->method('getDesactivateOpentalentSiteWeb')->willReturn(false);
+        $parameters->method('getCustomDomain')->willReturn(null);
+
+        $organization = $this->getMockBuilder(Organization::class)->getMock();
+        $organization->method('getParameters')->willReturn($parameters);
+
+        $organizationUtils->method('getOrganizationActiveSubdomain')->with($organization)->willReturn('outofnet');
+
+        $this->assertEquals('https://opentalent.fr', $organizationUtils->getOrganizationWebsite($organization));
+    }
+
+    /**
+     * @see Utils::isLastParentAndCMF()
+     */
+    public function testIsLastParentAndCMF(): void
+    {
+        $organizationUtils = $this->getOrganizationUtilsMockFor('isLastParentAndCMF');
+
+        $organization1 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization2 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization3 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization4 = $this->getMockBuilder(Organization::class)->getMock();
+
+        $this->networkOrganizationRepository
+            ->method('isLastParent')
+            ->willReturnMap([
+                [$organization1, true],
+                [$organization2, true],
+                [$organization3, false],
+                [$organization4, false],
+            ]);
+
+        $this->networkUtils
+            ->method('isCMF')
+            ->willReturnMap([
+                [$organization1, true],
+                [$organization2, false],
+                [$organization3, true],
+                [$organization4, false],
+            ]);
+
+        $this->assertTrue($organizationUtils->isLastParentAndCMF($organization1));
+        $this->assertFalse($organizationUtils->isLastParentAndCMF($organization2));
+        $this->assertFalse($organizationUtils->isLastParentAndCMF($organization3));
+        $this->assertFalse($organizationUtils->isLastParentAndCMF($organization4));
+    }
+
+    /**
+     * @see Utils::isStructureAndCMF()
+     */
+    public function testIsStructureAndCMF(): void
+    {
+        $organizationUtils = $this->getOrganizationUtilsMockFor('isStructureAndCMF');
+
+        $organization1 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization2 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization3 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization4 = $this->getMockBuilder(Organization::class)->getMock();
+
+        $organizationUtils
+            ->method('isStructure')
+            ->willReturnMap([
+                [$organization1, true],
+                [$organization2, false],
+                [$organization3, true],
+                [$organization4, false],
+            ]);
+
+        $this->networkUtils
+            ->method('isCMF')
+            ->with($organization1)
+            ->willReturnMap([
+                [$organization1, true],
+                [$organization2, false],
+                [$organization3, false],
+                [$organization4, true],
+            ]);
+
+        $this->assertTrue($organizationUtils->isStructureAndCMF($organization1));
+        $this->assertFalse($organizationUtils->isStructureAndCMF($organization2));
+        $this->assertFalse($organizationUtils->isStructureAndCMF($organization3));
+        $this->assertFalse($organizationUtils->isStructureAndCMF($organization4));
+    }
+
+    /**
+     * @see Utils::isManagerAndCMF()
+     */
+    public function testIsManagerAndCMF(): void
+    {
+        $organizationUtils = $this->getOrganizationUtilsMockFor('isManagerAndCMF');
+
+        $organization1 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization2 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization3 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization4 = $this->getMockBuilder(Organization::class)->getMock();
+
+        $organizationUtils
+            ->method('isManager')
+            ->willReturnMap([
+                [$organization1, true],
+                [$organization2, false],
+                [$organization3, true],
+                [$organization4, false],
+            ]);
+
+        $this->networkUtils
+            ->method('isCMF')
+            ->willReturnMap([
+                [$organization1, true],
+                [$organization2, false],
+                [$organization3, false],
+                [$organization4, true],
+            ]);
+
+        $this->assertTrue($organizationUtils->isManagerAndCMF($organization1));
+        $this->assertFalse($organizationUtils->isManagerAndCMF($organization2));
+        $this->assertFalse($organizationUtils->isManagerAndCMF($organization3));
+        $this->assertFalse($organizationUtils->isManagerAndCMF($organization4));
+    }
+
+    /**
+     * @see Utils::isManagerAndNotLastParentAndCMF()
+     */
+    public function testIsManagerAndLastParentAndCMF(): void
+    {
+        $organizationUtils = $this->getOrganizationUtilsMockFor('isManagerAndLastParentAndCMF');
+
+        $organization1 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization2 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization3 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization4 = $this->getMockBuilder(Organization::class)->getMock();
+
+        $organizationUtils
+            ->method('isManager')
+            ->willReturnMap([
+                [$organization1, true],
+                [$organization2, false],
+                [$organization3, true],
+                [$organization4, false],
+            ]);
+
+        $organizationUtils
+            ->method('isLastParentAndCMF')
+            ->willReturnMap([
+                [$organization1, true],
+                [$organization2, false],
+                [$organization3, false],
+                [$organization4, true],
+            ]);
+
+        $this->assertTrue($organizationUtils->isManagerAndLastParentAndCMF($organization1));
+        $this->assertFalse($organizationUtils->isManagerAndLastParentAndCMF($organization2));
+        $this->assertFalse($organizationUtils->isManagerAndLastParentAndCMF($organization3));
+        $this->assertFalse($organizationUtils->isManagerAndLastParentAndCMF($organization4));
+    }
+
+    /**
+     * @see Utils::isManagerAndNotLastParentAndCMF()
+     */
+    public function testIsManagerAndNotLastParentAndCMF(): void
+    {
+        $organizationUtils = $this->getOrganizationUtilsMockFor('isManagerAndNotLastParentAndCMF');
+
+        $organization1 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization2 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization3 = $this->getMockBuilder(Organization::class)->getMock();
+        $organization4 = $this->getMockBuilder(Organization::class)->getMock();
+
+        $organizationUtils
+            ->method('isManager')
+            ->willReturnMap([
+                [$organization1, true],
+                [$organization2, false],
+                [$organization3, true],
+                [$organization4, true],
+            ]);
+
+        $this->networkOrganizationRepository
+            ->method('isLastParent')
+            ->willReturnMap([
+                [$organization1, false],
+                [$organization2, false],
+                [$organization3, true],
+                [$organization4, false],
+            ]);
+
+        $this->networkUtils
+            ->method('isCMF')
+            ->willReturnMap([
+                [$organization1, true],
+                [$organization2, true],
+                [$organization3, true],
+                [$organization4, false],
+            ]);
+
+        $this->assertTrue($organizationUtils->isManagerAndNotLastParentAndCMF($organization1));
+        $this->assertFalse($organizationUtils->isManagerAndNotLastParentAndCMF($organization2));
+        $this->assertFalse($organizationUtils->isManagerAndNotLastParentAndCMF($organization3));
+        $this->assertFalse($organizationUtils->isManagerAndNotLastParentAndCMF($organization4));
+    }
+
     /**
      * @see OrganizationUtils::isSchool()
      */
     public function testIsSchool(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['isSchool'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('isSchool');
 
         $organization = $this->createOrganizationWithProductMock(SettingsProductEnum::ARTIST);
         $this->assertFalse($organizationUtils->isSchool($organization));
@@ -381,7 +616,7 @@ class UtilsTest extends TestCase
      */
     public function testIsArtist(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['isArtist'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('isArtist');
 
         $organization = $this->createOrganizationWithProductMock(SettingsProductEnum::ARTIST);
         $this->assertTrue($organizationUtils->isArtist($organization));
@@ -407,7 +642,7 @@ class UtilsTest extends TestCase
      */
     public function testHasModule(): void
     {
-        $organizationUtils = $this->getMockBuilder(TestableOrganizationUtils::class)->setMethodsExcept(['hasModule'])->getMock();
+        $organizationUtils = $this->getOrganizationUtilsMockFor('hasModule');
 
         $settings = $this->getMockBuilder(Settings::class)->getMock();
         $settings->method('getModules')->willReturn(['foo', 'bar']);
@@ -418,15 +653,4 @@ class UtilsTest extends TestCase
         $this->assertTrue($organizationUtils->hasModule($organization, 'foo'));
         $this->assertFalse($organizationUtils->hasModule($organization, 'other'));
     }
-
-    private function createOrganizationWithProductMock(SettingsProductEnum $product): Organization
-    {
-        $settings = $this->getMockBuilder(Settings::class)->getMock();
-        $settings->method('getProduct')->willReturn($product);
-
-        $organization = $this->getMockBuilder(Organization::class)->getMock();
-        $organization->method('getSettings')->willReturn($settings);
-
-        return $organization;
-    }
 }