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

freemium architecture & organization

Vincent 5 месяцев назад
Родитель
Сommit
4de1568498
37 измененных файлов с 760 добавлено и 102 удалено
  1. 31 29
      composer.json
  2. 55 46
      config/opentalent/products.yaml
  3. 3 2
      config/packages/api_platform.yaml
  4. 3 0
      config/packages/property_info.yaml
  5. 3 0
      config/packages/security.yaml
  6. 3 0
      config/routes/dh_auditor.yaml
  7. 12 0
      config/routes/nelmio_api_doc.yaml
  8. 0 0
      public/.htaccess
  9. 0 0
      public/fonts/CaviarDreams/CaviarDreams.ttf
  10. 0 0
      public/fonts/CaviarDreams/CaviarDreams_Bold.ttf
  11. 0 0
      public/fonts/CaviarDreams/CaviarDreams_BoldItalic.ttf
  12. 0 0
      public/fonts/CaviarDreams/CaviarDreams_Italic.ttf
  13. 0 0
      public/images/missing-file.png
  14. 0 0
      public/index.php
  15. 44 0
      src/ApiResources/Freemium/FreemiumEvent.php
  16. 97 0
      src/ApiResources/Freemium/FreemiumOrganization.php
  17. 14 0
      src/ApiResources/Utils/GpsCoordinate.php
  18. 0 5
      src/Doctrine/Booking/CurrentCoursesExtension.php
  19. 41 0
      src/Doctrine/Booking/CurrentEventsExtension.php
  20. 4 0
      src/Entity/Organization/Organization.php
  21. 37 0
      src/Entity/Organization/Traits/OrganizationComputedTraits.php
  22. 1 0
      src/Enum/Organization/SettingsProductEnum.php
  23. 2 2
      src/Filter/ApiPlatform/Utils/FindInSetFilter.php
  24. 2 2
      src/Filter/ApiPlatform/Utils/InFilter.php
  25. 1 1
      src/Security/Voter/EntityVoter/AbstractEntityVoter.php
  26. 118 0
      src/Service/ApiResourceBuilder/Freemium/FreemiumOrganizationBuilder.php
  27. 4 0
      src/Service/Doctrine/FiltersConfigurationService.php
  28. 1 1
      src/Service/Security/Module.php
  29. 17 2
      src/Service/Utils/GpsCoordinateUtils.php
  30. 55 0
      src/State/Processor/Freemium/FreemiumOrganizationProcessor.php
  31. 72 0
      src/State/Provider/Freemium/FreemiumEventProvider.php
  32. 44 0
      src/State/Provider/Freemium/FreemiumOrganizationProvider.php
  33. 54 0
      src/State/Provider/ProviderUtils.php
  34. 23 3
      src/State/Provider/Utils/GpsCoordinateSearchingProvider.php
  35. 2 4
      tests/Unit/Doctrine/Booking/CurrentCoursesExtensionTest.php
  36. 2 2
      tests/Unit/Service/Security/ModuleTest.php
  37. 15 3
      tests/Unit/Service/Utils/GpsCoordinateUtilsTest.php

+ 31 - 29
composer.json

@@ -11,7 +11,8 @@
     "php": ">=8.2",
     "ext-ctype": "*",
     "ext-iconv": "*",
-    "api-platform/core": "^4.0",
+    "api-platform/core": "^4.1",
+    "api-platform/graphql": "*",
     "beberlei/doctrineextensions": "^1.3",
     "composer/package-versions-deprecated": "^1.11",
     "damienharper/auditor-bundle": "^6.2",
@@ -38,33 +39,34 @@
     "phpstan/phpdoc-parser": "^1.16",
     "ramsey/uuid": "^4.2",
     "ramsey/uuid-doctrine": "^2.0",
-    "symfony/asset": "7.2.*",
-    "symfony/console": "7.2.*",
-    "symfony/doctrine-messenger": "7.2.*",
-    "symfony/dotenv": "7.2.*",
-    "symfony/error-handler": "7.2.*",
-    "symfony/expression-language": "7.2.*",
+    "symfony/asset": "7.3.*",
+    "symfony/console": "7.3.*",
+    "symfony/doctrine-messenger": "7.3.*",
+    "symfony/dotenv": "7.3.*",
+    "symfony/error-handler": "7.3.*",
+    "symfony/expression-language": "7.3.*",
     "symfony/flex": "^1.3.1",
-    "symfony/framework-bundle": "7.2.*",
-    "symfony/http-client": "7.2.*",
-    "symfony/intl": "7.2.*",
-    "symfony/lock": "7.2.*",
-    "symfony/mailer": "7.2.*",
+    "symfony/framework-bundle": "7.3.*",
+    "symfony/http-client": "7.3.*",
+    "symfony/intl": "7.3.*",
+    "symfony/lock": "7.3.*",
+    "symfony/mailer": "7.3.*",
     "symfony/mercure": "^0.6.1",
     "symfony/mercure-bundle": "^0.3.4",
-    "symfony/messenger": "7.2.*",
+    "symfony/messenger": "7.3.*",
     "symfony/monolog-bundle": "^3.7",
+    "symfony/object-mapper": "7.3.*",
     "symfony/polyfill-intl-icu": "^1.21",
     "symfony/polyfill-intl-messageformatter": "^1.24",
-    "symfony/property-access": "7.2.*",
-    "symfony/property-info": "7.2.*",
-    "symfony/security-bundle": "7.2.*",
-    "symfony/serializer": "7.2.*",
-    "symfony/translation": "7.2.*",
-    "symfony/twig-bundle": "7.2.*",
-    "symfony/uid": "7.2.*",
-    "symfony/validator": "7.2.*",
-    "symfony/yaml": "7.2.*",
+    "symfony/property-access": "7.3.*",
+    "symfony/property-info": "7.3.*",
+    "symfony/security-bundle": "7.3.*",
+    "symfony/serializer": "7.3.*",
+    "symfony/translation": "7.3.*",
+    "symfony/twig-bundle": "7.3.*",
+    "symfony/uid": "7.3.*",
+    "symfony/validator": "7.3.*",
+    "symfony/yaml": "7.3.*",
     "twig/cssinliner-extra": "^3.20",
     "twig/extra-bundle": "^3.20",
     "twig/inky-extra": "^3.20",
@@ -87,13 +89,13 @@
     "phpstan/phpstan-symfony": "^2.0",
     "phpunit/phpunit": "^9.6",
     "rector/rector": "^2.0",
-    "symfony/browser-kit": "7.2.*",
-    "symfony/css-selector": "7.2.*",
-    "symfony/debug-bundle": "7.2.*",
+    "symfony/browser-kit": "7.3.*",
+    "symfony/css-selector": "7.3.*",
+    "symfony/debug-bundle": "7.3.*",
     "symfony/maker-bundle": "^1.48",
-    "symfony/phpunit-bridge": "7.2.*",
-    "symfony/stopwatch": "7.2.*",
-    "symfony/web-profiler-bundle": "7.2.*",
+    "symfony/phpunit-bridge": "7.3.*",
+    "symfony/stopwatch": "7.3.*",
+    "symfony/web-profiler-bundle": "7.3.*",
     "theofidry/alice-data-fixtures": "1.7.2",
     "timeweb/phpstan-enum": "^4.0",
     "zenstruck/foundry": "2.3"
@@ -153,7 +155,7 @@
   "extra": {
     "symfony": {
       "allow-contrib": false,
-      "require": "7.2.*"
+      "require": "7.3.*"
     },
     "phpstan": {
       "includes": [

+ 55 - 46
config/opentalent/products.yaml

@@ -1,38 +1,43 @@
 parameters:
   opentalent.modules:
       Core:
-        entities:
+        resources:
           - AccessProfile
-          - Preferences
           - Tips
           - Notification
           - NotificationUser
-          - ContactPoint
-          - PersonalizedList
           - File
           - Image
           - City
           - Country
-          - Tagg
           - Enum
+          - Upload
+          - Download
+          - GpsCoordinate
+      Freemium:
+        resources:
+          - FreemiumOrganization
+          - FreemiumProfile
+          - FreemiumEvent
+          - AccessProfile
+      Common:
+        resources:
+          - Preferences
+          - ContactPoint
+          - PersonalizedList
+          - Tagg
           - LicenceCmfOrganizationER
           - UploadRequest
           - SubdomainAvailability
           - UserSearchItem
           - DolibarrDocDownload
-          - Download
-          - Upload
         roles:
           - ROLE_IMPORT
           - ROLE_TAGG
           - ROLE_WEBSITE
 
-      CorePremium:
-        entities:
-          - Tips
-
       GeneralConfig:
-        entities:
+        resources:
           - Place
           - PlaceSystem
           - PlaceControl
@@ -54,7 +59,7 @@ parameters:
           - ROLE_GENERAL_CONFIG
 
       Users:
-        entities:
+        resources:
           - Access
           - Commission
           - File
@@ -71,25 +76,25 @@ parameters:
           - ROLE_ACCOUNTS
 
       Commissons:
-         entities:
+         resources:
            - Commission
          roles:
            - ROLE_COMMISSIONS
 
       UsersSchool:
-        entities:
+        resources:
           - EducationStudent
           - Course
           - Education
 
       Donors:
-        entities:
+        resources:
           - Donor
         roles:
           - ROLE_DONORS
 
       Messages:
-        entities:
+        resources:
           - Message
           - Email
           - ReportMessage
@@ -97,35 +102,35 @@ parameters:
           - ROLE_EMAILS
 
       MessagesAdvanced:
-        entities:
+        resources:
           - Mail
         roles:
           - ROLE_MAILS
 
       Sms:
-        entities:
+        resources:
           - Sms
           - MobytUserStatus
         roles:
           - ROLE_TEXTO
 
       TemplateMessages:
-        entities:
+        resources:
           - TemplateSystem
 
       Tagg:
-        entities:
+        resources:
           - Tagg
         roles:
           - ROLE_TAGG
 
       TaggAdvanced:
-        entities: ~
+        resources: ~
         roles:
           - ROLE_TAGG_ADVANCED
 
       Events:
-          entities:
+          resources:
             - Event
             - EventGender
             - EventUser
@@ -137,7 +142,7 @@ parameters:
             - ROLE_EVENTS
 
       Courses:
-          entities:
+          resources:
             - Course
             - Work
             - WorkByUser
@@ -146,28 +151,28 @@ parameters:
             - ROLE_COURSES
 
       Examens:
-          entities:
+          resources:
             - Examen
             - Jury
           roles:
             - ROLE_EXAMENS
 
       EducationalProjects:
-          entities:
+          resources:
             - EducationalProject
             - EducationalProjectPublic
           roles:
             - ROLE_EDUCATIONALPROJECTS
 
       Attendances:
-          entities:
+          resources:
             - Attendance
             - AttendanceBooking
           roles:
             - ROLE_ATTENDANCES
 
       Equipments:
-          entities:
+          resources:
             - Equipment
             - EquipmentControl
             - EquipmentLoan
@@ -178,7 +183,7 @@ parameters:
             - ROLE_EQUIPMENTS
 
       PedagogicsAdministation:
-          entities:
+          resources:
             - EducationTeacher
             - EducationStudent
             - EducationCurriculum
@@ -193,18 +198,18 @@ parameters:
             - ROLE_PEDAGOGICS_ADMINISTRATION
 
       PedagogicsSeizure:
-          entities:
+          resources:
             - EducationStudent
           roles:
             - ROLE_PEDAGOGICS_SEIZURE
 
       AdvancedEducationNotation:
-        entities:
+        resources:
           - EducationNotationConfig
           - EducationNotationCriteriaConfig
 
       BillingAdministration:
-          entities:
+          resources:
             - Intangible
             - ResidenceArea
             - FamilyQuotient
@@ -221,11 +226,11 @@ parameters:
             - ROLE_BILLINGS_SEIZURE
 
       Pes:
-          entities:
+          resources:
             - Pes
 
       IEL:
-          entities:
+          resources:
             - AccessWish
             - AccessFamilyWish
             - AccessTmp
@@ -237,51 +242,51 @@ parameters:
             - ROLE_ONLINEREGISTRATION_ADMINISTRATION
 
       BergerLevrault:
-          entities:
+          resources:
             - BergerLevrault
 
       Jvs:
-          entities:
+          resources:
             - Jvs
 
       Website:
-          entities: ~
+          resources: ~
           roles:
             - ROLE_WEBSITE
 
       Statistic:
-        entities: ~
+        resources: ~
         roles:
           - ROLE_STATISTIC
 
       NetworkOrganization:
-          entities:
+          resources:
             - NetworkOrganization
 
       Network:
-          entities:
+          resources:
             - Network
           roles:
             - ROLE_NETWORK
 
       Cotisation:
-        entities:
+        resources:
           - Cotisation
 
       Dolibarr:
-        entities:
+        resources:
           - DolibarrAccount
 
       AccessReward:
         roles:
           - ROLE_ACCESSREWARD
-        entities:
+        resources:
           - AccessReward
 
       Reward:
         roles:
           - ROLE_REWARD
-        entities:
+        resources:
           - Reward
 
       Basicompta:
@@ -289,9 +294,14 @@ parameters:
           - ROLE_BASICOMPTA
 
   opentalent.products:
+      freemium:
+        modules:
+          - Freemium
+          - Core
       artist:
         modules:
           - Core
+          - Common
           - Users
           - Events
           - GeneralConfig
@@ -335,5 +345,4 @@ parameters:
 
       manager-premium:
         extend: manager
-        modules:
-          - CorePremium
+        modules: ~

+ 3 - 2
config/packages/api_platform.yaml

@@ -1,8 +1,9 @@
 api_platform:
     title: 'Opentalent API'
     version: '2.5'
-    enable_swagger_ui: false
-    enable_re_doc: false
+    enable_docs: true
+    enable_swagger: true
+    enable_swagger_ui: true
     mapping:
         paths:
             - '%kernel.project_dir%/src/Entity'

+ 3 - 0
config/packages/property_info.yaml

@@ -0,0 +1,3 @@
+framework:
+    property_info:
+        with_constructor_extractor: true

+ 3 - 0
config/packages/security.yaml

@@ -126,6 +126,9 @@ security:
             - ROLE_CORE
             - ROLE_RULERZ_ACTION
 
+        ROLE_USER_FREEMIUM:
+            - ROLE_CORE
+
     password_hashers:
         App\Entity\Person\Person:
             algorithm: bcrypt

+ 3 - 0
config/routes/dh_auditor.yaml

@@ -0,0 +1,3 @@
+dh_auditor:
+    resource: "@DHAuditorBundle/Controller/"
+    type: auditor

+ 12 - 0
config/routes/nelmio_api_doc.yaml

@@ -0,0 +1,12 @@
+# Expose your documentation as JSON swagger compliant
+app.swagger:
+    path: /api/doc.json
+    methods: GET
+    defaults: { _controller: nelmio_api_doc.controller.swagger }
+
+## Requires the Asset component and the Twig bundle
+## $ composer require twig asset
+#app.swagger_ui:
+#    path: /api/doc
+#    methods: GET
+#    defaults: { _controller: nelmio_api_doc.controller.swagger_ui }

+ 0 - 0
public/.htaccess


+ 0 - 0
public/fonts/CaviarDreams/CaviarDreams.ttf


+ 0 - 0
public/fonts/CaviarDreams/CaviarDreams_Bold.ttf


+ 0 - 0
public/fonts/CaviarDreams/CaviarDreams_BoldItalic.ttf


+ 0 - 0
public/fonts/CaviarDreams/CaviarDreams_Italic.ttf


+ 0 - 0
public/images/missing-file.png


+ 0 - 0
public/index.php


+ 44 - 0
src/ApiResources/Freemium/FreemiumEvent.php

@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\ApiResources\Freemium;
+
+use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
+use ApiPlatform\Metadata\ApiFilter;
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
+use App\ApiResources\ApiResourcesInterface;
+use App\Entity\Booking\Event;
+use App\State\Provider\Freemium\FreemiumEventProvider;
+use Symfony\Component\ObjectMapper\Attribute\Map;
+
+/**
+ * Classe resource contient tous les champs pour la gestion d'un événement pour un profile Freemium.
+ */
+#[ApiResource(
+    operations: [
+        new GetCollection(
+            uriTemplate: '/freemium/events',
+            security: 'is_granted("ROLE_USER_FREEMIUM")',
+            provider: FreemiumEventProvider::class
+        ),
+        new Get(
+            uriTemplate: '/freemium/events/{id}',
+            security: '(is_granted("ROLE_USER_FREEMIUM") and (object.organizationId == user.getOrganization().getId()))',
+            provider: FreemiumEventProvider::class
+        ),
+    ]
+)]
+#[ApiFilter(SearchFilter::class, properties: ['name' => 'partial'])]
+#[Map(source: Event::class)]
+class FreemiumEvent implements ApiResourcesInterface
+{
+    #[ApiProperty(identifier: true)]
+    public ?int $id = null;
+    #[Map(source: 'organization.id')]
+    public int $organizationName;
+    public ?string $name = null;
+}

+ 97 - 0
src/ApiResources/Freemium/FreemiumOrganization.php

@@ -0,0 +1,97 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\ApiResources\Freemium;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\Patch;
+use App\ApiResources\ApiResourcesInterface;
+use App\Entity\Core\Country;
+use App\Entity\Core\File;
+use App\Entity\Organization\Organization;
+use App\State\Processor\Freemium\FreemiumOrganizationProcessor;
+use App\State\Provider\Freemium\FreemiumOrganizationProvider;
+use libphonenumber\PhoneNumber;
+use Symfony\Component\ObjectMapper\Attribute\Map;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * Classe resource contient tous les champs pour la gestion d'une structure avec un compte freemium.
+ */
+#[ApiResource(
+    operations: [
+        new Get(
+            uriTemplate: '/freemium/organization',
+            security: 'is_granted("ROLE_USER_FREEMIUM")',
+            provider: FreemiumOrganizationProvider::class
+        ),
+        new Patch(
+            uriTemplate: '/freemium/organization',
+            security: 'is_granted("ROLE_USER_FREEMIUM")',
+            processor: FreemiumOrganizationProcessor::class
+        ),
+    ]
+)]
+#[Map(source: Organization::class)]
+class FreemiumOrganization implements ApiResourcesInterface
+{
+    #[ApiProperty(identifier: true)]
+    public ?int $id = null;
+
+    #[Assert\Length(max: 128)]
+    public ?string $name = null;
+
+    public ?string $description = null;
+
+    #[Map(source: 'principalContactPoint?.email')]
+    #[Assert\Email(message: 'invalid-email-format', mode: 'strict')]
+    #[Assert\Length(max: 255)]
+    public ?string $email = null;
+
+    #[Map(source: 'principalContactPoint?.telphone')]
+    public ?PhoneNumber $tel = null;
+
+    #[Map(source: 'principalAddressPostal?.addressPostal?.streetAddress')]
+    public ?string $streetAddress = null;
+
+    #[Map(source: 'principalAddressPostal?.addressPostal?.streetAddressSecond')]
+    public ?string $streetAddressSecond = null;
+
+    #[Map(source: 'principalAddressPostal?.addressPostal?.streetAddressThird')]
+    public ?string $streetAddressThird = null;
+
+    #[Map(source: 'principalAddressPostal?.addressPostal?.postalCode')]
+    public ?string $postalCode = null;
+
+    #[Map(source: 'principalAddressPostal?.addressPostal?.addressCity')]
+    public ?string $addressCity = null;
+
+    #[Map(source: 'principalAddressPostal?.addressPostal?.addressCountry')]
+    public ?Country $addressCountry = null;
+
+    #[Map(source: 'principalAddressPostal?.addressPostal?.latitude')]
+    public ?float $latitude = null;
+
+    #[Map(source: 'principalAddressPostal?.addressPostal?.longitude')]
+    public ?float $longitude = null;
+
+    #[Assert\Length(max: 255)]
+    public ?string $facebook = null;
+
+    #[Assert\Length(max: 255)]
+    public ?string $twitter = null;
+
+    #[Assert\Length(max: 255)]
+    public ?string $youtube = null;
+
+    #[Assert\Length(max: 255)]
+    public ?string $instagram = null;
+
+    public bool $portailVisibility = true;
+
+    #[Map(source: 'logo')]
+    public ?File $logo = null;
+}

+ 14 - 0
src/ApiResources/Utils/GpsCoordinate.php

@@ -29,6 +29,8 @@ class GpsCoordinate implements ApiResourcesInterface
     #[ApiProperty(identifier: true)]
     private float $longitude;
 
+    private ?string $displayName = null;
+
     private ?string $streetAddress = null;
 
     private ?string $streetAddressSecond = null;
@@ -76,6 +78,18 @@ class GpsCoordinate implements ApiResourcesInterface
         return $this->streetAddress;
     }
 
+    public function setDisplayName(?string $displayName): self
+    {
+        $this->displayName = $displayName;
+
+        return $this;
+    }
+
+    public function getDisplayName(): ?string
+    {
+        return $this->displayName;
+    }
+
     public function setStreetAddress(?string $streetAddress): self
     {
         $this->streetAddress = $streetAddress;

+ 0 - 5
src/Doctrine/Booking/CurrentCoursesExtension.php

@@ -25,9 +25,6 @@ final class CurrentCoursesExtension extends AbstractExtension
         return $resourceClass === Course::class;
     }
 
-    /**
-     * @todo : A la suite de la migration, il faut supprimer le where avec le discr.
-     */
     protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, ?Operation $operation): void
     {
         /** @var Access $currentUser */
@@ -37,9 +34,7 @@ final class CurrentCoursesExtension extends AbstractExtension
         }
         $rootAlias = $queryBuilder->getRootAliases()[0];
         $queryBuilder
-            ->andWhere(sprintf('%s.discr = :discr', $rootAlias))
             ->andWhere(sprintf('%s.organization = :organization', $rootAlias))
-            ->setParameter('discr', 'course')
             ->setParameter('organization', $currentUser->getOrganization())
         ;
     }

+ 41 - 0
src/Doctrine/Booking/CurrentEventsExtension.php

@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Doctrine\Booking;
+
+use ApiPlatform\Metadata\Operation;
+use App\Doctrine\AbstractExtension;
+use App\Entity\Access\Access;
+use App\Entity\Booking\Event;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Bundle\SecurityBundle\Security;
+
+/**
+ * Class CurrentEventsExtension : Filtre de sécurité par défaut pour une resource Event.
+ */
+final class CurrentEventsExtension extends AbstractExtension
+{
+    public function __construct(private Security $security)
+    {
+    }
+
+    public function supports(string $resourceClass, ?Operation $operation): bool
+    {
+        return $resourceClass === Event::class;
+    }
+
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, ?Operation $operation): void
+    {
+        /** @var Access $currentUser */
+        $currentUser = $this->security->getUser();
+        if ($currentUser === null || $currentUser->getOrganization() === null) {
+            return;
+        }
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder
+            ->andWhere(sprintf('%s.organization = :organization', $rootAlias))
+            ->setParameter('organization', $currentUser->getOrganization())
+        ;
+    }
+}

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

@@ -37,6 +37,7 @@ use App\Entity\Message\Email;
 use App\Entity\Message\Mail;
 use App\Entity\Message\Sms;
 use App\Entity\Network\NetworkOrganization;
+use App\Entity\Organization\Traits\OrganizationComputedTraits;
 use App\Entity\Person\Commission;
 use App\Entity\Place\Place;
 use App\Entity\Product\Equipment;
@@ -77,6 +78,7 @@ use JetBrains\PhpStorm\Pure;
 class Organization
 {
     use CreatedOnAndByTrait;
+    use OrganizationComputedTraits;
 
     #[ORM\Id]
     #[ORM\Column]
@@ -245,6 +247,7 @@ class Organization
      * @var Collection<int, ContactPoint>
      */
     #[ORM\ManyToMany(targetEntity: ContactPoint::class, mappedBy: 'organization', cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OrderBy(['id' => 'ASC'])]
     private Collection $contactPoints;
 
     /**
@@ -259,6 +262,7 @@ class Organization
 
     /** @var Collection<int, OrganizationAddressPostal> */
     #[ORM\OneToMany(mappedBy: 'organization', targetEntity: OrganizationAddressPostal::class, cascade: ['persist', 'remove'])]
+    #[ORM\OrderBy(['id' => 'ASC'])]
     private Collection $organizationAddressPostals;
 
     /** @var Collection<int, OrganizationLicence> */

+ 37 - 0
src/Entity/Organization/Traits/OrganizationComputedTraits.php

@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Entity\Organization\Traits;
+
+use App\Entity\Core\ContactPoint;
+use App\Entity\Organization\OrganizationAddressPostal;
+use App\Enum\Core\ContactPointTypeEnum;
+use App\Enum\Organization\AddressPostalOrganizationTypeEnum;
+
+trait OrganizationComputedTraits
+{
+    public function getPrincipalContactPoint(): ?ContactPoint
+    {
+        return $this->getContactPointByType(ContactPointTypeEnum::PRINCIPAL);
+    }
+
+    public function getContactPointByType(ContactPointTypeEnum $type): ?ContactPoint
+    {
+        return $this->contactPoints->filter(
+            fn (ContactPoint $cp) => $cp->getContactType() === $type
+        )->first() ?: null;
+    }
+
+    public function getPrincipalAddressPostal(): ?OrganizationAddressPostal
+    {
+        return $this->getAddressPostalByType(AddressPostalOrganizationTypeEnum::ADDRESS_HEAD_OFFICE);
+    }
+
+    public function getAddressPostalByType(AddressPostalOrganizationTypeEnum $type): ?OrganizationAddressPostal
+    {
+        return $this->organizationAddressPostals->filter(
+            fn (OrganizationAddressPostal $organizationAddressPostal) => $organizationAddressPostal->getType() === $type
+        )->first() ?: null;
+    }
+}

+ 1 - 0
src/Enum/Organization/SettingsProductEnum.php

@@ -13,6 +13,7 @@ enum SettingsProductEnum: string
 {
     use EnumMethodsTrait;
 
+    case FREEMIUM = 'freemium';
     case ARTIST = 'artist';
     case ARTIST_PREMIUM = 'artist-premium';
     case SCHOOL = 'school';

+ 2 - 2
src/Filter/ApiPlatform/Utils/FindInSetFilter.php

@@ -8,7 +8,7 @@ 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;
+use Symfony\Component\TypeInfo\Type;
 
 final class FindInSetFilter extends AbstractFilter
 {
@@ -51,7 +51,7 @@ final class FindInSetFilter extends AbstractFilter
         foreach ($this->properties as $property => $strategy) {
             $description["$property"] = [
                 'property' => $property,
-                'type' => Type::BUILTIN_TYPE_STRING,
+                'type' => 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",

+ 2 - 2
src/Filter/ApiPlatform/Utils/InFilter.php

@@ -8,7 +8,7 @@ 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;
+use Symfony\Component\TypeInfo\Type;
 
 /**
  * Is property included in the given CSV array.
@@ -63,7 +63,7 @@ final class InFilter extends AbstractFilter
         foreach ($this->properties as $property => $strategy) {
             $description[$property.'[in]'] = [
                 'property' => $property,
-                'type' => Type::BUILTIN_TYPE_STRING,
+                'type' => Type::string(),
                 'required' => false,
                 'swagger' => [
                     'description' => 'Filtre permettant d\'utiliser les IN. (usage: `id[in]=1,2,3`)',

+ 1 - 1
src/Security/Voter/EntityVoter/AbstractEntityVoter.php

@@ -71,7 +71,7 @@ abstract class AbstractEntityVoter extends Voter
             throw new \RuntimeException('Setup the self::$entityClass property, or override the supports() method');
         }
 
-        return $subject !== null && $subject::class === static::$entityClass && in_array($attribute, static::$allowedOperations);
+        return $subject !== null && $subject instanceof static::$entityClass && in_array($attribute, static::$allowedOperations);
     }
 
     /**

+ 118 - 0
src/Service/ApiResourceBuilder/Freemium/FreemiumOrganizationBuilder.php

@@ -0,0 +1,118 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\ApiResourceBuilder\Freemium;
+
+use App\ApiResources\Freemium\FreemiumOrganization;
+use App\Entity\Core\AddressPostal;
+use App\Entity\Core\ContactPoint;
+use App\Entity\Organization\Organization;
+use App\Entity\Organization\OrganizationAddressPostal;
+use App\Enum\Core\ContactPointTypeEnum;
+use App\Enum\Organization\AddressPostalOrganizationTypeEnum;
+
+class FreemiumOrganizationBuilder
+{
+    /**
+     * Mapping des informations.
+     *
+     * @param Organization         $organization         : objet target
+     * @param FreemiumOrganization $freemiumOrganization : objet source
+     */
+    public function mapInformations(Organization $organization, FreemiumOrganization $freemiumOrganization): void
+    {
+        // Mapping des infos principales
+        $this->mapOrganizationInformations($organization, $freemiumOrganization);
+
+        // Mapping des infos du point de contact principal
+        $this->mapContactPointInformations($this->getPrincipalContactPointOrCreateNewOne($organization), $freemiumOrganization);
+
+        // Mapping des infos du point de l'adresse principale
+        $this->mapAddressPostalInformations($this->getPrincipalAddressPostalOrCreateNewOne($organization), $freemiumOrganization);
+    }
+
+    /**
+     * Mapping des informations de Organization depuis FreemiumOrganization.
+     *
+     * @param Organization         $organization         : objet target
+     * @param FreemiumOrganization $freemiumOrganization : objet source
+     */
+    private function mapOrganizationInformations(Organization $organization, FreemiumOrganization $freemiumOrganization): void
+    {
+        $organization->setName($freemiumOrganization->name);
+        $organization->setDescription($freemiumOrganization->description);
+        $organization->setFacebook($freemiumOrganization->facebook);
+        $organization->setYoutube($freemiumOrganization->youtube);
+        $organization->setInstagram($freemiumOrganization->instagram);
+        $organization->setTwitter($freemiumOrganization->twitter);
+        $organization->setPortailVisibility($freemiumOrganization->portailVisibility);
+        $organization->setLogo($freemiumOrganization->logo);
+    }
+
+    /**
+     * Mapping des informations de ContactPoint depuis FreemiumOrganization.
+     *
+     * @param ContactPoint         $contactPoint         : objet target
+     * @param FreemiumOrganization $freemiumOrganization : objet source
+     */
+    private function mapContactPointInformations(ContactPoint $contactPoint, FreemiumOrganization $freemiumOrganization): void
+    {
+        $contactPoint->setTelphone($freemiumOrganization->tel);
+        $contactPoint->setEmail($freemiumOrganization->email);
+    }
+
+    /**
+     * Mapping des informations de AddressPostal depuis FreemiumOrganization.
+     *
+     * @param AddressPostal        $address              : objet target
+     * @param FreemiumOrganization $freemiumOrganization : objet source
+     */
+    private function mapAddressPostalInformations(AddressPostal $address, FreemiumOrganization $freemiumOrganization): void
+    {
+        $address->setStreetAddress($freemiumOrganization->streetAddress);
+        $address->setStreetAddressSecond($freemiumOrganization->streetAddressSecond);
+        $address->setStreetAddressThird($freemiumOrganization->streetAddressThird);
+        $address->setPostalCode($freemiumOrganization->postalCode);
+        $address->setAddressCity($freemiumOrganization->addressCity);
+        $address->setAddressCountry($freemiumOrganization->addressCountry);
+        $address->setLongitude($freemiumOrganization->longitude);
+        $address->setLatitude($freemiumOrganization->latitude);
+    }
+
+    /**
+     * On récupère le point de contact principal de l'organisation. Si elle n'existe pas on l'à créer.
+     */
+    private function getPrincipalContactPointOrCreateNewOne(Organization $organization): ContactPoint
+    {
+        $principalContactPoint = $organization->getPrincipalContactPoint();
+        if ($principalContactPoint) {
+            return $principalContactPoint;
+        }
+
+        $principalContactPoint = new ContactPoint();
+        $principalContactPoint->setContactType(ContactPointTypeEnum::PRINCIPAL);
+        $organization->addContactPoint($principalContactPoint);
+
+        return $principalContactPoint;
+    }
+
+    /**
+     * On récupère l'adresse principale de l'organisation. Si elle n'existe pas on l'à créer.
+     */
+    private function getPrincipalAddressPostalOrCreateNewOne(Organization $organization): AddressPostal
+    {
+        $principalAddressPostal = $organization->getPrincipalAddressPostal();
+        if ($principalAddressPostal) {
+            return $principalAddressPostal->getAddressPostal();
+        }
+
+        $principalAddressPostal = new OrganizationAddressPostal();
+        $principalAddressPostal->setType(AddressPostalOrganizationTypeEnum::ADDRESS_HEAD_OFFICE);
+        $address = new AddressPostal();
+        $principalAddressPostal->setAddressPostal($address);
+        $organization->addOrganizationAddressPostal($principalAddressPostal);
+
+        return $address;
+    }
+}

+ 4 - 0
src/Service/Doctrine/FiltersConfigurationService.php

@@ -116,6 +116,10 @@ class FiltersConfigurationService
             throw new \RuntimeException('time constraints has not been suspended, can not be restored');
         }
 
+        if ($this->previousTimeConstraintState === false) {
+            return;
+        }
+
         $this->enableFilter('date_time_filter');
         $this->enableFilter('activity_year_filter');
 

+ 1 - 1
src/Service/Security/Module.php

@@ -154,7 +154,7 @@ class Module
     {
         $modules = $this->parameterBag->get('opentalent.modules');
         foreach ($modules as $module => $data) {
-            if ($data['entities'] && in_array($resource, $data['entities'], true)) {
+            if ($data['resources'] && in_array($resource, $data['resources'], true)) {
                 return $module;
             }
         }

+ 17 - 2
src/Service/Utils/GpsCoordinateUtils.php

@@ -37,10 +37,10 @@ class GpsCoordinateUtils
      *
      * @see GpsCoordinateUtilsTest::testSearchGpsCoordinates()
      */
-    public function searchGpsCoordinates(?string $street, ?string $cp, ?string $city): mixed
+    public function searchGpsCoordinates(?string $street, ?string $cp, ?string $city, ?string $country): mixed
     {
         try {
-            $url = sprintf('search?addressdetails=1&format=json&limit=10&street=%s&postalcode=%s&city=%s', $street, $cp, $city);
+            $url = sprintf('search?addressdetails=1&format=json&limit=10&%s', $this->prepareQuery($street, $cp, $city, $country));
             $response = $this->clientOpenStreetMap->request('GET', $url)->getContent();
         } catch (\Exception $e) {
             throw new NotFoundHttpException('no_reverse_gps_coordinate', $e, 404);
@@ -49,6 +49,21 @@ class GpsCoordinateUtils
         return json_decode($response, true, 512, JSON_THROW_ON_ERROR);
     }
 
+    /**
+     * Prépare la query pour l'api de recherche gps.
+     */
+    public function prepareQuery(?string $street, ?string $cp, ?string $city, ?string $country): string
+    {
+        $query = [
+            $street ? sprintf('street=%s', $street) : null,
+            $cp ? sprintf('postalcode=%s', $cp) : null,
+            $city ? sprintf('city=%s', $city) : null,
+            $country ? sprintf('country=%s', $country) : null,
+        ];
+
+        return implode('&', array_filter($query));
+    }
+
     /**
      * Renvoi l'adresse correspondant à la latitude et longitude demandée.
      *

+ 55 - 0
src/State/Processor/Freemium/FreemiumOrganizationProcessor.php

@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\State\Processor\Freemium;
+
+use ApiPlatform\Metadata\Delete;
+use ApiPlatform\Metadata\Operation;
+use App\ApiResources\Freemium\FreemiumOrganization;
+use App\Entity\Access\Access;
+use App\Service\ApiResourceBuilder\Freemium\FreemiumOrganizationBuilder;
+use App\State\Processor\EntityProcessor;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Classe FreemiumOrganizationProcessor qui est un custom dataPersister gérant la resource FreemiumOrganization.
+ */
+class FreemiumOrganizationProcessor extends EntityProcessor
+{
+    public function __construct(
+        private Security $security,
+        private EntityManagerInterface $entityManager,
+        private FreemiumOrganizationBuilder $freemiumOrganizationBuilder,
+    ) {
+    }
+
+    /**
+     * @param mixed[] $uriVariables
+     * @param mixed[] $context
+     *
+     * @throws \Exception
+     */
+    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): FreemiumOrganization
+    {
+        if ($operation instanceof Delete) {
+            throw new \RuntimeException('not supported', Response::HTTP_METHOD_NOT_ALLOWED);
+        }
+
+        /** @var FreemiumOrganization $organizationFreemiumRequest */
+        $organizationFreemiumRequest = $data;
+
+        /** @var Access $access */
+        $access = $this->security->getUser();
+        $organization = $access->getOrganization();
+
+        $this->freemiumOrganizationBuilder->mapInformations($organization, $organizationFreemiumRequest);
+
+        $this->entityManager->persist($organization);
+        $this->entityManager->flush();
+
+        return $organizationFreemiumRequest;
+    }
+}

+ 72 - 0
src/State/Provider/Freemium/FreemiumEventProvider.php

@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\State\Provider\Freemium;
+
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProviderInterface;
+use App\ApiResources\Freemium\FreemiumEvent;
+use App\Entity\Booking\Event;
+use App\State\Provider\ProviderUtils;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\ObjectMapper\ObjectMapperInterface;
+
+/**
+ * Class AccessProfileProvider : custom provider pour assurer l'alimentation de la réponse du GET my_profile.
+ */
+final class FreemiumEventProvider implements ProviderInterface
+{
+    public function __construct(
+        private ProviderUtils $providerUtils,
+        private ObjectMapperInterface $objectMapper,
+        private EntityManagerInterface $em,
+    ) {
+    }
+
+    /**
+     * @param array<mixed> $uriVariables
+     * @param array<mixed> $context
+     *
+     * @throws \Doctrine\ORM\Exception\ORMException
+     * @throws \Doctrine\ORM\OptimisticLockException
+     */
+    public function provide(Operation $operation, array $uriVariables = [], array $context = []): \Generator|FreemiumEvent|null
+    {
+        if ($operation instanceof GetCollection) {
+            return $this->provideCollection($operation, $context);
+        }
+
+        return $this->provideItem($uriVariables, $context);
+    }
+
+    /**
+     * @param array<mixed> $context
+     */
+    private function provideCollection(Operation $operation, array $context): \Generator
+    {
+        $apiPlatformPaginator = $this->providerUtils->applyCollectionExtensionsAndPagination(Event::class, $operation, $context);
+
+        foreach ($apiPlatformPaginator as $event) {
+            yield $this->objectMapper->map($event, FreemiumEvent::class);
+        }
+    }
+
+    /**
+     * @param array<mixed> $uriVariables
+     * @param array<mixed> $context
+     *
+     * @throws \Doctrine\ORM\Exception\ORMException
+     * @throws \Doctrine\ORM\OptimisticLockException
+     */
+    private function provideItem(array $uriVariables, array $context): ?FreemiumEvent
+    {
+        $event = $this->em->find(Event::class, $uriVariables['id']);
+        if (!$event) {
+            return null;
+        }
+
+        return $this->objectMapper->map($event, FreemiumEvent::class);
+    }
+}

+ 44 - 0
src/State/Provider/Freemium/FreemiumOrganizationProvider.php

@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\State\Provider\Freemium;
+
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProviderInterface;
+use App\ApiResources\Freemium\FreemiumOrganization;
+use App\Entity\Access\Access;
+use App\Entity\Organization\Organization;
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\ObjectMapper\ObjectMapperInterface;
+
+/**
+ * Class FreemiumOrganizationProvider : custom provider pour assurer l'alimentation de la réponse du GET freemium/organization.
+ */
+final class FreemiumOrganizationProvider implements ProviderInterface
+{
+    public function __construct(
+        private Security $security,
+        private ObjectMapperInterface $objectMapper,
+    ) {
+    }
+
+    /**
+     * @param array<mixed> $uriVariables
+     * @param array<mixed> $context
+     */
+    public function provide(Operation $operation, array $uriVariables = [], array $context = []): FreemiumOrganization
+    {
+        if ($operation instanceof GetCollection) {
+            throw new \RuntimeException('not supported', Response::HTTP_METHOD_NOT_ALLOWED);
+        }
+        /** @var Access $access */
+        $access = $this->security->getUser();
+        /** @var Organization $organization */
+        $organization = $access->getOrganization();
+
+        return $this->objectMapper->map($organization, FreemiumOrganization::class);
+    }
+}

+ 54 - 0
src/State/Provider/ProviderUtils.php

@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\State\Provider;
+
+use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
+use ApiPlatform\Doctrine\Orm\Paginator as ApiPlatformPaginator;
+use ApiPlatform\Doctrine\Orm\Util\QueryNameGenerator;
+use ApiPlatform\Metadata\Operation;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator;
+use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
+
+class ProviderUtils
+{
+    /**
+     * @param iterable<QueryCollectionExtensionInterface> $collectionExtensions
+     */
+    public function __construct(
+        private EntityManagerInterface $em,
+        #[AutowireIterator(tag: 'api_platform.doctrine.orm.query_extension.collection')]
+        private iterable $collectionExtensions,
+    ) {
+    }
+
+    /**
+     * Applique les extension natives d'api platform (filtres, pagination, etc.).
+     *
+     * @param array<mixed> $context
+     */
+    public function applyCollectionExtensionsAndPagination(string $class, Operation $operation, array $context = []): ApiPlatformPaginator
+    {
+        $qb = $this->em->getRepository($class)->createQueryBuilder('o');
+
+        $queryNameGenerator = new QueryNameGenerator();
+
+        foreach ($this->collectionExtensions as $extension) {
+            if ($extension instanceof QueryCollectionExtensionInterface) {
+                $extension->applyToCollection(
+                    $qb,
+                    $queryNameGenerator,
+                    $class,
+                    $operation,
+                    $context
+                );
+            }
+        }
+
+        $doctrinePaginator = new DoctrinePaginator($qb);
+
+        return new ApiPlatformPaginator($doctrinePaginator);
+    }
+}

+ 23 - 3
src/State/Provider/Utils/GpsCoordinateSearchingProvider.php

@@ -83,12 +83,32 @@ final class GpsCoordinateSearchingProvider implements ProviderInterface
         try {
             if ($request) {
                 $addresses = $this->gpsCoordinateUtils->searchGpsCoordinates(
-                    $request->get('street'),
+                    trim(sprintf('%s %s %s',
+                        $request->get('streetAddress'),
+                        $request->get('streetAddressSecond'),
+                        $request->get('streetAddressThird')
+                    )),
                     $request->get('cp'),
-                    $request->get('city')
+                    $request->get('city'),
+                    $request->get('country')
                 );
+
+                // Si aucune adresses ne ressort, alors on rentente sans la partie "street"
+                if (empty($addresses)) {
+                    $addresses = $this->gpsCoordinateUtils->searchGpsCoordinates(
+                        null,
+                        $request->get('cp'),
+                        $request->get('city'),
+                        $request->get('country')
+                    );
+                }
+
                 foreach ($addresses as $address) {
-                    $responses[] = $this->gpsCoordinateUtils->createGpsCoordinate($address);
+                    $responses[] = (new GpsCoordinate())
+                        ->setDisplayName($address['display_name'])
+                        ->setLongitude((float) $address['lon'])
+                        ->setLatitude((float) $address['lat'])
+                    ;
                 }
             }
         } catch (\Exception) {

+ 2 - 4
tests/Unit/Doctrine/Booking/CurrentCoursesExtensionTest.php

@@ -37,18 +37,16 @@ class CurrentCoursesExtensionTest extends ExtensionTestCase
         $this->queryBuilder->method('getRootAliases')->willReturn(['a']);
 
         $this->queryBuilder
-            ->expects(self::exactly(2))
+            ->expects(self::exactly(1))
             ->method('andWhere')
             ->willReturnMap([
-                ['a.discr = :discr', $this->queryBuilder],
                 ['a.organization = :organization', $this->queryBuilder],
             ]);
 
         $this->queryBuilder
-            ->expects(self::exactly(2))
+            ->expects(self::exactly(1))
             ->method('setParameter')
             ->willReturnMap([
-                ['discr', 'course', null, $this->queryBuilder],
                 ['organization', $this->organization, null, $this->queryBuilder],
             ]);
 

+ 2 - 2
tests/Unit/Service/Security/ModuleTest.php

@@ -207,7 +207,7 @@ class ModuleTest extends TestCase
         $module = $this->getMockForMethod('getModuleByResourceName');
 
         $this->parameterBag->method('get')->with('opentalent.modules')->willReturn(
-            ['Core' => ['entities' => ['foo', 'bar']]]
+            ['Core' => ['resources' => ['foo', 'bar']]]
         );
 
         $this->assertEquals('Core', $module->getModuleByResourceName('foo'));
@@ -221,7 +221,7 @@ class ModuleTest extends TestCase
         $module = $this->getMockForMethod('getModuleByResourceName');
 
         $this->parameterBag->method('get')->with('opentalent.modules')->willReturn(
-            ['Core' => ['entities' => ['bar']]]
+            ['Core' => ['resources' => ['bar']]]
         );
 
         $this->assertNull($module->getModuleByResourceName('foo'));

+ 15 - 3
tests/Unit/Service/Utils/GpsCoordinateUtilsTest.php

@@ -30,16 +30,28 @@ class GpsCoordinateUtilsTest extends TestCase
         $response = $this->getMockBuilder(ResponseInterface::class)->getMock();
         $response->method('getContent')->willReturn('{"data":1}');  // dummy response data
 
+        $street = '11 chemin des rirets';
+        $cp = '74300';
+        $city = 'nancy-sur-cluses';
+        $country = 'france';
+
         $this->client
             ->expects(self::once())
             ->method('request')
             ->with(
                 'GET',
-                'search?addressdetails=1&format=json&limit=10&street=11 chemin des rirtes&postalcode=74300&city=nancy-sur-cluses'
+                'search?addressdetails=1&format=json&limit=10&street=11 chemin des rirets&postalcode=74300&city=nancy-sur-cluses&country=france'
             )
             ->willReturn($response);
 
-        $content = $gpsCoordinateUtils->searchGpsCoordinates('11 chemin des rirtes', '74300', 'nancy-sur-cluses');
+        $gpsCoordinateUtils
+            ->expects(self::once())
+            ->method('prepareQuery')
+            ->with($street, $cp, $city, $country)
+            ->willReturn('street=11 chemin des rirets&postalcode=74300&city=nancy-sur-cluses&country=france')
+        ;
+
+        $content = $gpsCoordinateUtils->searchGpsCoordinates($street, $cp, $city, $country);
 
         $this->assertEquals(['data' => 1], $content);
     }
@@ -60,7 +72,7 @@ class GpsCoordinateUtilsTest extends TestCase
             ->willThrowException(new \Exception());
 
         $this->expectException(NotFoundHttpException::class);
-        $gpsCoordinateUtils->searchGpsCoordinates('...', '74300', '...');
+        $gpsCoordinateUtils->searchGpsCoordinates('...', '74300', '...', 'france');
     }
 
     /**