Pārlūkot izejas kodu

Merge branch 'feature/freemium' into develop

Vincent 6 mēneši atpakaļ
vecāks
revīzija
56db6ec36e
63 mainītis faili ar 1790 papildinājumiem un 120 dzēšanām
  1. 31 29
      composer.json
  2. 1 0
      config/opentalent/enum.yaml
  3. 58 46
      config/opentalent/products.yaml
  4. 10 2
      config/packages/api_platform.yaml
  5. 12 6
      config/packages/liip_imagine.yaml
  6. 3 0
      config/packages/property_info.yaml
  7. 3 0
      config/packages/security.yaml
  8. 3 0
      config/routes/dh_auditor.yaml
  9. 12 0
      config/routes/nelmio_api_doc.yaml
  10. 0 0
      public/.htaccess
  11. 0 0
      public/fonts/CaviarDreams/CaviarDreams.ttf
  12. 0 0
      public/fonts/CaviarDreams/CaviarDreams_Bold.ttf
  13. 0 0
      public/fonts/CaviarDreams/CaviarDreams_BoldItalic.ttf
  14. 0 0
      public/fonts/CaviarDreams/CaviarDreams_Italic.ttf
  15. 0 0
      public/images/missing-file.png
  16. 0 0
      public/index.php
  17. 0 0
      src/ApiResource/.gitignore
  18. 37 0
      src/ApiResources/Core/EventCategory.php
  19. 153 0
      src/ApiResources/Freemium/FreemiumEvent.php
  20. 96 0
      src/ApiResources/Freemium/FreemiumOrganization.php
  21. 62 0
      src/ApiResources/Freemium/FreemiumPlace.php
  22. 48 0
      src/ApiResources/Search/PlaceSearchItem.php
  23. 14 0
      src/ApiResources/Utils/GpsCoordinate.php
  24. 0 1
      src/Doctrine/Access/AdditionalExtension/DateTimeConstraintExtensionAdditional.php
  25. 0 5
      src/Doctrine/Booking/CurrentCoursesExtension.php
  26. 41 0
      src/Doctrine/Booking/CurrentEventsExtension.php
  27. 41 0
      src/Doctrine/Place/CurrentPlaceExtension.php
  28. 86 0
      src/Entity/Booking/Event.php
  29. 1 1
      src/Entity/Core/Categories.php
  30. 15 0
      src/Entity/Core/Familly.php
  31. 15 0
      src/Entity/Core/Gender.php
  32. 16 0
      src/Entity/Core/Subfamilly.php
  33. 4 0
      src/Entity/Organization/Organization.php
  34. 37 0
      src/Entity/Organization/Traits/OrganizationComputedTraits.php
  35. 15 0
      src/Entity/Place/AbstractPlace.php
  36. 3 1
      src/Entity/Place/Place.php
  37. 18 0
      src/Enum/Booking/PricingEventEnum.php
  38. 1 0
      src/Enum/Organization/SettingsProductEnum.php
  39. 2 8
      src/EventListener/OnKernelRequestPreRead.php
  40. 2 2
      src/Filter/ApiPlatform/Utils/FindInSetFilter.php
  41. 2 2
      src/Filter/ApiPlatform/Utils/InFilter.php
  42. 18 0
      src/Repository/Place/PlaceRepository.php
  43. 1 1
      src/Security/Voter/EntityVoter/AbstractEntityVoter.php
  44. 137 0
      src/Service/ApiResourceBuilder/Freemium/FreemiumEventBuilder.php
  45. 118 0
      src/Service/ApiResourceBuilder/Freemium/FreemiumOrganizationBuilder.php
  46. 4 0
      src/Service/Doctrine/FiltersConfigurationService.php
  47. 1 1
      src/Service/Security/Module.php
  48. 2 1
      src/Service/Utils/EntityUtils.php
  49. 17 2
      src/Service/Utils/GpsCoordinateUtils.php
  50. 61 0
      src/State/Processor/Freemium/FreemiumEventProcessor.php
  51. 55 0
      src/State/Processor/Freemium/FreemiumOrganizationProcessor.php
  52. 68 0
      src/State/Provider/Core/EventCategoryProvider.php
  53. 94 0
      src/State/Provider/Freemium/FreemiumEventProvider.php
  54. 44 0
      src/State/Provider/Freemium/FreemiumOrganizationProvider.php
  55. 60 0
      src/State/Provider/Freemium/FreemiumPlaceProvider.php
  56. 74 0
      src/State/Provider/ProviderUtils.php
  57. 76 0
      src/State/Provider/Search/PlaceSearchItemProvider.php
  58. 23 3
      src/State/Provider/Utils/GpsCoordinateSearchingProvider.php
  59. 34 0
      src/Validator/Constraints/LessThanField.php
  60. 42 0
      src/Validator/Constraints/LessThanFieldValidator.php
  61. 2 4
      tests/Unit/Doctrine/Booking/CurrentCoursesExtensionTest.php
  62. 2 2
      tests/Unit/Service/Security/ModuleTest.php
  63. 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": [

+ 1 - 0
config/opentalent/enum.yaml

@@ -118,6 +118,7 @@ parameters:
           event_timeline: 'App\Enum\Booking\EventTimelineTypeEnum'
           educational_project_timeline: 'App\Enum\Booking\EducationalProjectTimelineTypeEnum'
           event_participation: 'App\Enum\Booking\ParticipationStatusEnum'
+          pricing_event: 'App\Enum\Booking\PricingEventEnum'
 
         # File
           file_visibility: 'App\Enum\Core\FileVisibilityEnum'

+ 58 - 46
config/opentalent/products.yaml

@@ -1,38 +1,46 @@
 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
+          - FreemiumPlace
+          - AccessProfile
+          - EventCategory
+          - PlaceSearchItem
+      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 +62,7 @@ parameters:
           - ROLE_GENERAL_CONFIG
 
       Users:
-        entities:
+        resources:
           - Access
           - Commission
           - File
@@ -71,25 +79,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 +105,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 +145,7 @@ parameters:
             - ROLE_EVENTS
 
       Courses:
-          entities:
+          resources:
             - Course
             - Work
             - WorkByUser
@@ -146,28 +154,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 +186,7 @@ parameters:
             - ROLE_EQUIPMENTS
 
       PedagogicsAdministation:
-          entities:
+          resources:
             - EducationTeacher
             - EducationStudent
             - EducationCurriculum
@@ -193,18 +201,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 +229,11 @@ parameters:
             - ROLE_BILLINGS_SEIZURE
 
       Pes:
-          entities:
+          resources:
             - Pes
 
       IEL:
-          entities:
+          resources:
             - AccessWish
             - AccessFamilyWish
             - AccessTmp
@@ -237,51 +245,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 +297,14 @@ parameters:
           - ROLE_BASICOMPTA
 
   opentalent.products:
+      freemium:
+        modules:
+          - Freemium
+          - Core
       artist:
         modules:
           - Core
+          - Common
           - Users
           - Events
           - GeneralConfig
@@ -335,5 +348,4 @@ parameters:
 
       manager-premium:
         extend: manager
-        modules:
-          - CorePremium
+        modules: ~

+ 10 - 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'
@@ -15,12 +16,19 @@ api_platform:
     resource_class_directories:
         - '%kernel.project_dir%/src/Entity'
     defaults:
+        pagination_enabled: true
         pagination_items_per_page: 20
+        pagination_maximum_items_per_page: 50
+        pagination_client_enabled: true
+        pagination_client_items_per_page: true
         normalization_context:
             ## In 3.0, in conformance with the JSON Merge Patch RFC, the default value of the skip_null_values
             ## property is true which means that from now on null values are omitted during serialization.
             ## we don't want this => surcharge default value to false
             skip_null_values: false
+    formats:
+        jsonld: [ 'application/ld+json' ]
+        json: [ 'application/json' ]
     graphql:
         graphql_playground: false
     use_symfony_listeners: true

+ 12 - 6
config/packages/liip_imagine.yaml

@@ -37,22 +37,28 @@ liip_imagine:
                     widen: 800
         crop_sm:
             filters:
+                auto_rotate: ~
                 crop:
                     size: ~
                     start: ~
-                relative_resize:
-                    widen: 100
+                thumbnail:
+                    size: [ 100, 100 ]
+                    mode: inset
         crop_md:
             filters:
+                auto_rotate: ~
                 crop:
                     size: ~
                     start: ~
-                relative_resize:
-                    widen: 300
+                thumbnail:
+                    size: [ 300, 200 ]
+                    mode: inset
         crop_lg:
             filters:
+                auto_rotate: ~
                 crop:
                     size: ~
                     start: ~
-                relative_resize:
-                    widen: 800
+                thumbnail:
+                    size: [ 800, 600 ]
+                    mode: inset

+ 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


+ 0 - 0
src/ApiResource/.gitignore


+ 37 - 0
src/ApiResources/Core/EventCategory.php

@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\ApiResources\Core;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\GetCollection;
+use App\ApiResources\ApiResourcesInterface;
+use App\State\Provider\Core\EventCategoryProvider;
+
+/**
+ * Classe resource qui contient les champs disponibles lors d'un appel à event-category.
+ */
+#[ApiResource(
+    operations: [
+        new GetCollection(
+            uriTemplate: '/event-categories',
+            paginationEnabled: false,
+            provider: EventCategoryProvider::class
+        ),
+    ]
+)]
+class EventCategory implements ApiResourcesInterface
+{
+    #[ApiProperty(identifier: true)]
+    public ?int $id = null;
+
+    public ?string $label = null;
+
+    public ?string $famillyLabel = null;
+
+    public ?string $subfamillyLabel = null;
+
+    public ?string $genderLabel = null;
+}

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

@@ -0,0 +1,153 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\ApiResources\Freemium;
+
+use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
+use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
+use ApiPlatform\Metadata\ApiFilter;
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Delete;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Patch;
+use ApiPlatform\Metadata\Post;
+use App\ApiResources\ApiResourcesInterface;
+use App\Attribute\OrganizationDefaultValue;
+use App\Entity\Booking\Event;
+use App\Entity\Core\Categories;
+use App\Entity\Core\Country;
+use App\Entity\Core\File;
+use App\Entity\Organization\Organization;
+use App\Entity\Place\Place;
+use App\Enum\Booking\PricingEventEnum;
+use App\State\Processor\Freemium\FreemiumEventProcessor;
+use App\State\Provider\Freemium\FreemiumEventProvider;
+use Doctrine\Common\Collections\ArrayCollection;
+use JetBrains\PhpStorm\Pure;
+use Symfony\Component\ObjectMapper\Attribute\Map;
+use App\Validator\Constraints as OpentalentAssert;
+
+/**
+ * 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")',
+        ),
+        new Post(
+            uriTemplate: '/freemium/events',
+            security: 'is_granted("ROLE_USER_FREEMIUM")',
+        ),
+        new Get(
+            uriTemplate: '/freemium/events/{id}',
+            security: '(is_granted("ROLE_USER_FREEMIUM") and (object.organization == user.getOrganization()))',
+        ),
+        new Patch(
+            uriTemplate: '/freemium/events/{id}',
+            security: '(is_granted("ROLE_USER_FREEMIUM") and (object.organization == user.getOrganization()))',
+        ),
+        new Delete(
+            uriTemplate: '/freemium/events/{id}',
+            security: '(is_granted("ROLE_USER_FREEMIUM") and (object.organization == user.getOrganization()))',
+        ),
+    ],
+    provider: FreemiumEventProvider::class,
+    processor: FreemiumEventProcessor::class
+)]
+#[OrganizationDefaultValue(fieldName: 'organization')]
+#[ApiFilter(filterClass: DateFilter::class, properties: ['datetimeStart'])]
+#[ApiFilter(filterClass: OrderFilter::class, properties: ['datetimeStart'], arguments: ['orderParameterName' => 'order'])]
+#[Map(source: Event::class)]
+#[OpentalentAssert\LessThanField(field: 'datetimeStart', comparedTo: 'datetimeEnd')]
+class FreemiumEvent implements ApiResourcesInterface
+{
+    #[ApiProperty(identifier: true)]
+    public ?int $id = null;
+
+    public Organization $organization;
+
+    public string $name;
+
+    public ?\DateTimeInterface $datetimeStart = null;
+
+    public ?\DateTimeInterface $datetimeEnd = null;
+
+    public ?string $description = null;
+
+    public ?File $image = null;
+
+    public ?string $url = null;
+
+    public ?string $urlTicket = null;
+
+    public ?Place $place = null;
+
+    #[Map(source: 'place?.name')]
+    public ?string $placeName = null;
+
+    #[Map(source: 'place?.addressPostal?.streetAddress')]
+    public ?string $streetAddress = null;
+
+    #[Map(source: 'place?.addressPostal?.streetAddressSecond')]
+    public ?string $streetAddressSecond = null;
+
+    #[Map(source: 'place?.addressPostal?.streetAddressThird')]
+    public ?string $streetAddressThird = null;
+
+    #[Map(source: 'place?.addressPostal?.postalCode')]
+    public ?string $postalCode = null;
+
+    #[Map(source: 'place?.addressPostal?.addressCity')]
+    public ?string $addressCity = null;
+
+    #[Map(source: 'place?.addressPostal?.addressCountry')]
+    public ?Country $addressCountry = null;
+
+    #[Map(source: 'place?.addressPostal?.latitude')]
+    public ?float $latitude = null;
+
+    #[Map(source: 'place?.addressPostal?.longitude')]
+    public ?float $longitude = null;
+
+    #[Map(if: false)]
+    public ArrayCollection $categories;
+
+    public ?PricingEventEnum $pricing = null;
+
+    public ?float $priceMini = null;
+
+    public ?float $priceMaxi = null;
+
+    #[Pure]
+    public function __construct()
+    {
+        $this->categories = new ArrayCollection();
+    }
+
+    public function getCategories(): array
+    {
+        // retourne un tableau proprement indexé
+        return array_values($this->categories->toArray());
+    }
+
+    public function addCategory(Categories $categories): self
+    {
+        if (!$this->categories->contains($categories)) {
+            $this->categories[] = $categories;
+        }
+
+        return $this;
+    }
+
+    public function removeCategory(Categories $categories): self
+    {
+        $this->categories->removeElement($categories);
+
+        return $this;
+    }
+}

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

@@ -0,0 +1,96 @@
+<?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")'
+        ),
+        new Patch(
+            uriTemplate: '/freemium/organization',
+            security: 'is_granted("ROLE_USER_FREEMIUM")'
+        ),
+    ],
+    provider: FreemiumOrganizationProvider::class,
+    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;
+
+    public ?File $logo = null;
+}

+ 62 - 0
src/ApiResources/Freemium/FreemiumPlace.php

@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\ApiResources\Freemium;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use App\ApiResources\ApiResourcesInterface;
+use App\Entity\Core\Country;
+use App\Entity\Organization\Organization;
+use App\Entity\Place\Place;
+use App\State\Provider\Freemium\FreemiumPlaceProvider;
+use Symfony\Component\ObjectMapper\Attribute\Map;
+
+/**
+ * Classe resource contient tous les champs pour la gestion d'une place pour un profile Freemium.
+ */
+#[ApiResource(
+    operations: [
+        new Get(
+            uriTemplate: '/freemium/places/{id}',
+            security: '(is_granted("ROLE_USER_FREEMIUM") and (object.organization == user.getOrganization()))',
+        )
+    ],
+    provider: FreemiumPlaceProvider::class,
+)]
+#[Map(source: Place::class)]
+class FreemiumPlace implements ApiResourcesInterface
+{
+    #[ApiProperty(identifier: true)]
+    public ?int $id = null;
+
+    public Organization $organization;
+
+    public string $name;
+
+    #[Map(source: 'addressPostal?.streetAddress')]
+    public ?string $streetAddress = null;
+
+    #[Map(source: 'addressPostal?.streetAddressSecond')]
+    public ?string $streetAddressSecond = null;
+
+    #[Map(source: 'addressPostal?.streetAddressThird')]
+    public ?string $streetAddressThird = null;
+
+    #[Map(source: 'addressPostal?.postalCode')]
+    public ?string $postalCode = null;
+
+    #[Map(source: 'addressPostal?.addressCity')]
+    public ?string $addressCity = null;
+
+    #[Map(source: 'addressPostal?.addressCountry')]
+    public ?Country $addressCountry = null;
+
+    #[Map(source: 'addressPostal?.latitude')]
+    public ?float $latitude = null;
+
+    #[Map(source: 'addressPostal?.longitude')]
+    public ?float $longitude = null;
+}

+ 48 - 0
src/ApiResources/Search/PlaceSearchItem.php

@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\ApiResources\Search;
+
+use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
+use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
+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\Organization\Organization;
+use App\Entity\Place\Place;
+use App\Filter\ApiPlatform\Utils\InFilter;
+use App\State\Provider\Search\PlaceSearchItemProvider;
+use Symfony\Component\ObjectMapper\Attribute\Map;
+
+/**
+ * Classe resource pour les recherches de lieux
+ */
+#[ApiResource(
+    operations: [
+        new Get(
+            uriTemplate: '/search/places/{id}',
+            security: 'object.organization == user.getOrganization()'
+        ),
+        new GetCollection(
+            uriTemplate: '/search/places'
+        )
+    ],
+    provider: PlaceSearchItemProvider::class,
+)]
+#[ApiFilter(filterClass: SearchFilter::class, properties: ['name' => 'ipartial'])]
+#[ApiFilter(filterClass: OrderFilter::class, properties: ['name' => 'ASC'])]
+#[ApiFilter(filterClass: InFilter::class, properties: ['id'])]
+#[Map(source: Place::class)]
+class PlaceSearchItem implements ApiResourcesInterface
+{
+    #[ApiProperty(identifier: true)]
+    public ?int $id = null;
+
+    public string $name;
+
+    public Organization $organization;
+}

+ 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 - 1
src/Doctrine/Access/AdditionalExtension/DateTimeConstraintExtensionAdditional.php

@@ -23,7 +23,6 @@ class DateTimeConstraintExtensionAdditional implements AdditionalAccessExtension
         return
 
                 $this->requestStack->getMainRequest()->isMethod('GET')
-                && $this->requestStack->getMainRequest()->get('_time_constraint', true) == true
         ;
     }
 

+ 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())
+        ;
+    }
+}

+ 41 - 0
src/Doctrine/Place/CurrentPlaceExtension.php

@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Doctrine\Place;
+
+use ApiPlatform\Metadata\Operation;
+use App\Doctrine\AbstractExtension;
+use App\Entity\Access\Access;
+use App\Entity\Place\Place;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Bundle\SecurityBundle\Security;
+
+/**
+ * Class CurrentPlaceExtension : Filtre de sécurité par défaut pour une resource Place.
+ */
+final class CurrentPlaceExtension extends AbstractExtension
+{
+    public function __construct(private Security $security)
+    {
+    }
+
+    public function supports(string $resourceClass, ?Operation $operation): bool
+    {
+        return $resourceClass === Place::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())
+        ;
+    }
+}

+ 86 - 0
src/Entity/Booking/Event.php

@@ -12,6 +12,7 @@ use App\Entity\Place\Place;
 use App\Entity\Place\PlaceSystem;
 use App\Entity\Place\Room;
 // use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Auditable;
+use App\Enum\Booking\PricingEventEnum;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
@@ -88,6 +89,28 @@ class Event extends AbstractBooking
     #[ORM\OneToMany(targetEntity: AttendanceBooking::class, mappedBy: 'event', cascade: ['persist', 'remove'])]
     protected Collection $attendanceBooking;
 
+    #[ORM\Column(type: 'text', nullable: true)]
+    protected ?string $description;
+
+    #[ORM\Column]
+    #[Assert\Regex("/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/", "url-error")]
+    protected ?string $url = null;
+
+    #[ORM\Column]
+    #[Assert\Regex("/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/", "url-ticket-error")]
+    protected ?string $urlTicket = null;
+
+    #[ORM\Column(length: 255, nullable: true, enumType: PricingEventEnum::class)]
+    private ?PricingEventEnum $pricing = null;
+
+    #[ORM\Column]
+    #[Assert\Positive]
+    protected ?float $priceMini = null;
+
+    #[ORM\Column]
+    #[Assert\Positive]
+    protected ?float $priceMaxi = null;
+
     public function __construct()
     {
         $this->eventRecur = new ArrayCollection();
@@ -288,6 +311,15 @@ class Event extends AbstractBooking
         return $this;
     }
 
+    public function removeAllCategories(): self
+    {
+        foreach ($this->categories as $category) {
+            $this->removeCategory($category);
+        }
+
+        return $this;
+    }
+
     /**
      * @return Collection<int, EventReport>
      */
@@ -382,4 +414,58 @@ class Event extends AbstractBooking
 
         return $this;
     }
+
+    public function getDescription(): ?string{
+        return $this->description;
+    }
+
+    public function setDescription(?string $description): self{
+        $this->description = $description;
+        return $this;
+    }
+
+    public function getUrl(): ?string{
+        return $this->url;
+    }
+
+    public function setUrl(?string $url): self{
+        $this->url = $url;
+        return $this;
+    }
+
+    public function getUrlTicket(): ?string{
+        return $this->urlTicket;
+    }
+
+    public function setUrlTicket(?string $urlTicket): self{
+        $this->urlTicket = $urlTicket;
+        return $this;
+    }
+
+    public function getPricing(): ?PricingEventEnum{
+        return $this->pricing;
+    }
+
+    public function setPricing(?PricingEventEnum $pricing): self{
+        $this->pricing = $pricing;
+        return $this;
+    }
+
+    public function getPriceMini(): ?float{
+        return $this->priceMini;
+    }
+
+    public function setPriceMini(?float $priceMini): self{
+        $this->priceMini = $priceMini;
+        return $this;
+    }
+
+    public function getPriceMaxi(): ?float{
+        return $this->priceMaxi;
+    }
+
+    public function setPriceMaxi(?float $priceMaxi): self{
+        $this->priceMaxi = $priceMaxi;
+        return $this;
+    }
 }

+ 1 - 1
src/Entity/Core/Categories.php

@@ -17,7 +17,7 @@ use Doctrine\ORM\Mapping as ORM;
 class Categories
 {
     #[ORM\Id]
-    #[ORM\Column(type: 'mediumint', options: ['unsigned' => true])]
+    #[ORM\Column]
     #[ORM\GeneratedValue]
     private ?int $id = null;
 

+ 15 - 0
src/Entity/Core/Familly.php

@@ -21,8 +21,23 @@ class Familly
     #[ORM\GeneratedValue]
     private ?int $id = null;
 
+    #[ORM\Column(type: 'string')]
+    private string $name;
+
     public function getId(): ?int
     {
         return $this->id;
     }
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    public function setName(string $name): self
+    {
+        $this->name = $name;
+
+        return $this;
+    }
 }

+ 15 - 0
src/Entity/Core/Gender.php

@@ -21,8 +21,23 @@ class Gender
     #[ORM\GeneratedValue]
     private ?int $id = null;
 
+    #[ORM\Column(type: 'string')]
+    private string $name;
+
     public function getId(): ?int
     {
         return $this->id;
     }
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    public function setName(string $name): self
+    {
+        $this->name = $name;
+
+        return $this;
+    }
 }

+ 16 - 0
src/Entity/Core/Subfamilly.php

@@ -21,8 +21,24 @@ class Subfamilly
     #[ORM\GeneratedValue]
     private ?int $id = null;
 
+    #[ORM\Column(type: 'string')]
+    private string $name;
+
     public function getId(): ?int
     {
         return $this->id;
     }
+
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    public function setName(string $name): self
+    {
+        $this->name = $name;
+
+        return $this;
+    }
 }

+ 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;
+    }
+}

+ 15 - 0
src/Entity/Place/AbstractPlace.php

@@ -33,6 +33,9 @@ abstract class AbstractPlace
     #[ORM\GeneratedValue]
     private ?int $id = null;
 
+    #[ORM\Column]
+    protected string $name;
+
     #[ORM\ManyToOne(cascade: ['persist'], inversedBy: 'places')]
     #[ORM\JoinColumn(referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
     private ?AddressPostal $addressPostal;
@@ -54,6 +57,18 @@ abstract class AbstractPlace
         return $this->id;
     }
 
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    public function setName(string $name): self
+    {
+        $this->name = $name;
+
+        return $this;
+    }
+
     public function getAddressPostal(): ?AddressPostal
     {
         return $this->addressPostal;

+ 3 - 1
src/Entity/Place/Place.php

@@ -6,6 +6,7 @@ namespace App\Entity\Place;
 
 use ApiPlatform\Metadata\ApiResource;
 // use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Auditable;
+use ApiPlatform\Metadata\GetCollection;
 use App\Entity\Booking\Course;
 use App\Entity\Booking\EducationalProject;
 use App\Entity\Booking\Event;
@@ -13,6 +14,7 @@ use App\Entity\Booking\Examen;
 use App\Entity\Core\ContactPoint;
 use App\Entity\Organization\Organization;
 use App\Entity\Product\Equipment;
+use App\Repository\Place\PlaceRepository;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
@@ -22,7 +24,7 @@ use Doctrine\ORM\Mapping as ORM;
  */
 // #[Auditable]
 #[ApiResource(operations: [])]
-#[ORM\Entity]
+#[ORM\Entity(repositoryClass: PlaceRepository::class)]
 class Place extends AbstractPlace
 {
     /** @var Collection<int, Event> */

+ 18 - 0
src/Enum/Booking/PricingEventEnum.php

@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Enum\Booking;
+
+use App\Enum\EnumMethodsTrait;
+
+/**
+ * Prix des événements
+ */
+enum PricingEventEnum: string
+{
+    use EnumMethodsTrait;
+
+    case FREE = 'FREE';
+    case PAID = 'PAID';
+}

+ 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 - 8
src/EventListener/OnKernelRequestPreRead.php

@@ -37,17 +37,11 @@ class OnKernelRequestPreRead implements EventSubscriberInterface
             return;
         }
 
-        $mainRequest = $this->requestStack->getMainRequest();
-
         /** @var Access $access */
         $access = $this->security->getUser();
         if ($access) {
-            $timeConstraintEnabled = (bool) $this->requestStack->getMainRequest()->get('_time_constraint', true);
-
-            if ($timeConstraintEnabled) {
-                // Configure les filtres pour prendre en compte les contraintes temporelles
-                $this->filtersConfigurationService->configureTimeConstraintFilters($access->getId());
-            }
+            // Configure les filtres pour prendre en compte les contraintes temporelles
+            $this->filtersConfigurationService->configureTimeConstraintFilters($access->getId());
 
             $profileHash = $event->getRequest()->headers->get('profileHash');
             if ($profileHash !== null) {

+ 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`)',

+ 18 - 0
src/Repository/Place/PlaceRepository.php

@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Repository\Place;
+
+use App\Entity\Education\Cycle;
+use App\Entity\Place\Place;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+class PlaceRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, Place::class);
+    }
+}

+ 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);
     }
 
     /**

+ 137 - 0
src/Service/ApiResourceBuilder/Freemium/FreemiumEventBuilder.php

@@ -0,0 +1,137 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\ApiResourceBuilder\Freemium;
+
+use App\ApiResources\Freemium\FreemiumEvent;
+use App\Entity\Booking\Event;
+use App\Entity\Core\AddressPostal;
+use App\Entity\Place\Place;
+use Doctrine\ORM\EntityManagerInterface;
+
+class FreemiumEventBuilder
+{
+    public function __construct(
+        private EntityManagerInterface $em
+    )
+    {}
+
+    /**
+     * Mapping des informations.
+     *
+     * @param Event $event : objet target
+     * @param FreemiumEvent $freemiumEvent : objet source
+     */
+    public function mapInformations(Event $event, FreemiumEvent $freemiumEvent): FreemiumEvent
+    {
+        $this->mapEventInformations( $event, $freemiumEvent);
+        $this->mapEventPlaceInformations( $event, $freemiumEvent);
+        return $freemiumEvent;
+    }
+
+    /**
+     * Mapping des informations générales.
+     *
+     * @param Event $event : objet target
+     * @param FreemiumEvent $freemiumEvent : objet source
+     */
+    private function mapEventInformations(Event $event, FreemiumEvent $freemiumEvent)
+    {
+        //General informations
+        $event->setName($freemiumEvent->name);
+        $event->setOrganization($freemiumEvent->organization);
+        $event->setDatetimeStart($freemiumEvent->datetimeStart);
+        $event->setDatetimeEnd($freemiumEvent->datetimeEnd);
+        $event->setDescription($freemiumEvent->description);
+        $event->setImage($freemiumEvent->image);
+        $event->setUrl($freemiumEvent->url);
+        $event->setUrlTicket($freemiumEvent->urlTicket);
+        $event->setPricing($freemiumEvent->pricing);
+        $event->setPriceMini($freemiumEvent->priceMini);
+        $event->setPriceMaxi($freemiumEvent->priceMaxi);
+
+        //Catégories
+        $event->removeAllCategories();
+        foreach ($freemiumEvent->categories as $category) {
+            $event->addCategory($category);
+        }
+    }
+
+    /**
+     * Recherche et mapping du lieu de lévénement
+     * @param Event $event
+     * @param FreemiumEvent $freemiumEvent
+     * @return void
+     */
+    private function mapEventPlaceInformations(Event $event, FreemiumEvent $freemiumEvent){
+        $place = $this->getPlace($freemiumEvent);
+        if($place !== null){
+            $this->mapPlaceInformations($place, $freemiumEvent);
+            $this->em->persist($place);
+        }
+        $event->setPlace($place);
+    }
+
+    /**
+     * Mapping des informations du lieux et de son adresse postale
+     * @param FreemiumEvent $freemiumEvent
+     * @return Place|array|object[]
+     */
+    private function mapPlaceInformations(Place $place, FreemiumEvent $freemiumEvent): void
+    {
+        $addressPostal = $this->getAddressPostal($place);
+
+        //Mapping des informations de l'adresse
+        $addressPostal
+            ->setStreetAddress($freemiumEvent->streetAddress)
+            ->setStreetAddressSecond($freemiumEvent->streetAddressSecond)
+            ->setStreetAddressThird($freemiumEvent->streetAddressThird)
+            ->setPostalCode($freemiumEvent->postalCode)
+            ->setAddressCity($freemiumEvent->addressCity)
+            ->setAddressCountry($freemiumEvent->addressCountry)
+            ->setLatitude($freemiumEvent->latitude)
+            ->setLongitude($freemiumEvent->longitude);
+
+        //Mapping des informations du lieu
+        $place
+            ->setName($freemiumEvent->placeName)
+            ->setOrganization($freemiumEvent->organization)
+            ->setAddressPostal($addressPostal);
+    }
+
+    /**
+     * Récupération de la place si définie, sinon on en créer une si un minimum d'information est fournies
+     * @param FreemiumEvent $freemiumEvent
+     * @return Place|null
+     */
+    private function getPlace(FreemiumEvent $freemiumEvent): ?Place
+    {
+        if ($freemiumEvent->place) {
+            return $freemiumEvent->place;
+        } else if (
+            $freemiumEvent->placeName ||
+            $freemiumEvent->streetAddress ||
+            $freemiumEvent->streetAddressSecond ||
+            $freemiumEvent->streetAddressThird ||
+            $freemiumEvent->postalCode ||
+            $freemiumEvent->addressCity
+        ) {
+            return new Place();
+        }
+        return null;
+    }
+
+    /**
+     * Récupération de l'adresse postale si définie, sinon on en créer une nouvelle.
+     * @param Place $place
+     * @return AddressPostal
+     */
+    private function getAddressPostal(Place $place): AddressPostal
+    {
+        if ($place->getAddressPostal()) {
+            return $place->getAddressPostal();
+        }
+        return new AddressPostal();
+    }
+}

+ 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;
             }
         }

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

@@ -48,7 +48,8 @@ class EntityUtils
         $organizationDefaultValue = $reflection->getAttributes(OrganizationDefaultValue::class)[0] ?? null;
         $fieldName = $organizationDefaultValue?->getArguments()['fieldName'] ?? null;
         if ($fieldName) {
-            $entity->{sprintf('set%s', ucfirst($fieldName))}(...[$access->getOrganization()]);
+            $property = $reflection->getProperty($fieldName);
+            $property->setValue($entity, $access->getOrganization());
         }
     }
 

+ 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.
      *

+ 61 - 0
src/State/Processor/Freemium/FreemiumEventProcessor.php

@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\State\Processor\Freemium;
+
+use ApiPlatform\Metadata\Delete;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Metadata\Post;
+use App\ApiResources\Freemium\FreemiumEvent;
+use App\Entity\Booking\Event;
+use App\Repository\Booking\EventRepository;
+use App\Service\ApiResourceBuilder\Freemium\FreemiumEventBuilder;
+use App\State\Processor\EntityProcessor;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\ORM\EntityManagerInterface;
+
+/**
+ * Classe FreemiumEventProcessor qui est un custom dataPersister gérant la resource FreemiumEvent.
+ */
+class FreemiumEventProcessor extends EntityProcessor
+{
+    public function __construct(
+        private EntityManagerInterface $entityManager,
+        private FreemiumEventBuilder $freemiumEventBuilder,
+        private EventRepository $eventRepository,
+    ) {
+    }
+
+    /**
+     * @param FreemiumEvent $data
+     * @param Operation $operation
+     * @param array $uriVariables
+     * @param array $context
+     * @return FreemiumEvent
+     */
+    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): FreemiumEvent
+    {
+        if($operation instanceof Post){
+            $event = new Event();
+        }else{
+            $event = $this->eventRepository->find($uriVariables['id']);
+        }
+
+        if ($operation instanceof Delete) {
+            $this->entityManager->remove($event);
+            $freemiumEvent = new FreemiumEvent();
+        }else{
+            $freemiumEvent = $this->freemiumEventBuilder->mapInformations($event, $data);
+            $this->entityManager->persist($event);
+        }
+
+        $this->entityManager->flush();
+
+        if ($operation instanceof Post) {
+            $freemiumEvent->id = $event->getId();
+        }
+
+        return $freemiumEvent;
+    }
+}

+ 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;
+    }
+}

+ 68 - 0
src/State/Provider/Core/EventCategoryProvider.php

@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\State\Provider\Core;
+
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProviderInterface;
+use App\ApiResources\Core\EventCategory;
+use App\Entity\Core\Categories;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Class EventCategoryProvider : custom provider pour assurer l'alimentation de la ressource EventCategory.
+ */
+final class EventCategoryProvider implements ProviderInterface
+{
+    public function __construct(
+        private readonly EntityManagerInterface $entityManager,
+    ) {
+    }
+
+    /**
+     * @param mixed[] $uriVariables
+     * @param mixed[] $context
+     *
+     * @return EventCategory[]|EventCategory
+     */
+    public function provide(Operation $operation, array $uriVariables = [], array $context = []): array|EventCategory
+    {
+        if (!$operation instanceof GetCollection) {
+            throw new \RuntimeException('Only GetCollection operation is supported', Response::HTTP_METHOD_NOT_ALLOWED);
+        }
+
+        return $this->getCollection();
+    }
+
+    /**
+     * @return EventCategory[]
+     */
+    private function getCollection(): array
+    {
+        $categoriesRepository = $this->entityManager->getRepository(Categories::class);
+        $categories = $categoriesRepository->findAll();
+
+        $result = [];
+        foreach ($categories as $category) {
+            $eventCategory = new EventCategory();
+            $eventCategory->id = $category->getId();
+
+            // Get the related entities
+            $familly = $category->getFamilly();
+            $subfamilly = $category->getSubfamilly();
+            $gender = $category->getGender();
+
+            // Generate labels for the related entities based on their IDs
+            $eventCategory->famillyLabel = $familly->getName();
+            $eventCategory->subfamillyLabel = $subfamilly->getName();
+            $eventCategory->genderLabel = $gender->getName();
+
+            $result[] = $eventCategory;
+        }
+
+        return $result;
+    }
+}

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

@@ -0,0 +1,94 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\State\Provider\Freemium;
+
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\Pagination\TraversablePaginator;
+use ApiPlatform\State\ProviderInterface;
+use App\ApiResources\Freemium\FreemiumEvent;
+use App\Entity\Booking\Event;
+use App\Repository\Booking\EventRepository;
+use App\Service\Doctrine\FiltersConfigurationService;
+use App\State\Provider\ProviderUtils;
+use Doctrine\Common\Collections\ArrayCollection;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+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 EventRepository $eventRepository,
+        private FiltersConfigurationService $filtersConfigurationService
+    ) {
+    }
+
+    /**
+     * @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 = []): TraversablePaginator|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): TraversablePaginator
+    {
+        $this->filtersConfigurationService->suspendTimeConstraintFilters();
+
+        $originalPaginator = $this->providerUtils->applyCollectionExtensionsAndPagination(Event::class, $operation, $context);
+
+        $mappedItems = [];
+        foreach ($originalPaginator as $item) {
+            $mappedItems[]= $this->objectMapper->map($item, FreemiumEvent::class);
+        }
+
+        $this->filtersConfigurationService->restoreTimeConstraintFilters();
+
+        return $this->providerUtils->getTraversablePaginator($mappedItems, $originalPaginator);
+    }
+
+    /**
+     * @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
+    {
+        $this->filtersConfigurationService->suspendTimeConstraintFilters();
+        /** @var Event $event */
+        if(empty($event = $this->eventRepository->find($uriVariables['id']))){
+            throw new NotFoundHttpException('event not found');
+        }
+        $this->filtersConfigurationService->restoreTimeConstraintFilters();
+
+        $freemiumEvent = $this->objectMapper->map($event, FreemiumEvent::class);
+
+        //Afin de s'assurer que les catégories ne sont plus directement liées à l'Event source.
+        $categories = new ArrayCollection();;
+        foreach ($event->getCategories() as $cat){
+            $categories->add($cat);
+        }
+        $freemiumEvent->categories = $categories;
+
+        return $freemiumEvent;
+    }
+}

+ 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);
+    }
+}

+ 60 - 0
src/State/Provider/Freemium/FreemiumPlaceProvider.php

@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\State\Provider\Freemium;
+
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\Pagination\TraversablePaginator;
+use ApiPlatform\State\ProviderInterface;
+use App\ApiResources\Freemium\FreemiumPlace;
+use App\Entity\Place\Place;
+use App\Repository\Place\PlaceRepository;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\ObjectMapper\ObjectMapperInterface;
+
+/**
+ * Class FreemiumPlaceProvider : custom provider pour assurer l'alimentation de la réponse du GET freemium/places/{id}.
+ */
+final class FreemiumPlaceProvider implements ProviderInterface
+{
+    public function __construct(
+        private ObjectMapperInterface $objectMapper,
+        private PlaceRepository $placeRepository
+    ) {
+    }
+
+    /**
+     * @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 = []): TraversablePaginator|FreemiumPlace|null
+    {
+        if ($operation instanceof GetCollection) {
+            throw new \RuntimeException('not supported', Response::HTTP_METHOD_NOT_ALLOWED);
+        }
+        return $this->provideItem($uriVariables, $context);
+    }
+
+    /**
+     * @param array<mixed> $uriVariables
+     * @param array<mixed> $context
+     *
+     * @throws \Doctrine\ORM\Exception\ORMException
+     * @throws \Doctrine\ORM\OptimisticLockException
+     */
+    private function provideItem(array $uriVariables, array $context): ?FreemiumPlace
+    {
+        /** @var Place $place */
+        if(empty($place = $this->placeRepository->find($uriVariables['id']))){
+            throw new NotFoundHttpException('place not found');
+        }
+
+        return $this->objectMapper->map($place, FreemiumPlace::class);
+    }
+}

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

@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\State\Provider;
+
+use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
+use ApiPlatform\Doctrine\Orm\Util\QueryNameGenerator;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\Pagination\Pagination;
+use ApiPlatform\State\Pagination\TraversablePaginator;
+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,
+        private Pagination $pagination,
+    ) {
+    }
+
+    /**
+     * Applique les extension natives d'api platform (filtres, pagination, etc.).
+     *
+     * @param array<mixed> $context
+     */
+    public function applyCollectionExtensionsAndPagination(string $class, Operation $operation, array $context = []): TraversablePaginator
+    {
+        $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 TraversablePaginator(
+            $doctrinePaginator,
+            $this->pagination->getPage($context),
+            $this->pagination->getLimit($operation, $context),
+            count($doctrinePaginator) // Nombre total d'éléments pour générer "last", "next", etc.
+        );
+    }
+
+    /**
+     * @param $mappedItems
+     * @param $originalPaginator
+     * @return TraversablePaginator
+     */
+    public function getTraversablePaginator(array $mappedItems, TraversablePaginator $originalPaginator) : TraversablePaginator{
+        return new TraversablePaginator(
+            new \ArrayIterator($mappedItems),
+            $originalPaginator->getCurrentPage(),
+            $originalPaginator->getItemsPerPage(),
+            $originalPaginator->getTotalItems()
+        );
+    }
+}

+ 76 - 0
src/State/Provider/Search/PlaceSearchItemProvider.php

@@ -0,0 +1,76 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\State\Provider\Search;
+
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\Pagination\TraversablePaginator;
+use ApiPlatform\State\ProviderInterface;
+use App\ApiResources\Search\PlaceSearchItem;
+use App\Entity\Booking\Event;
+use App\Entity\Place\Place;
+use App\Repository\Place\PlaceRepository;
+use App\State\Provider\ProviderUtils;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\ObjectMapper\ObjectMapperInterface;
+
+/**
+ * Class PlaceSearchItemProvider : custom provider pour assurer l'alimentation de la réponse du GET search/places.
+ */
+final class PlaceSearchItemProvider implements ProviderInterface
+{
+    public function __construct(
+        private ProviderUtils $providerUtils,
+        private ObjectMapperInterface $objectMapper,
+        private PlaceRepository $placeRepository,
+    ) {
+    }
+
+    /**
+     * @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 = []): TraversablePaginator|PlaceSearchItem|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): TraversablePaginator
+    {
+        $originalPaginator = $this->providerUtils->applyCollectionExtensionsAndPagination(Place::class, $operation, $context);
+
+        $mappedItems = [];
+        foreach ($originalPaginator as $item) {
+            $mappedItems[]= $this->objectMapper->map($item, PlaceSearchItem::class);
+        }
+
+        return $this->providerUtils->getTraversablePaginator($mappedItems, $originalPaginator);
+    }
+
+    /**
+     * @param array<mixed> $uriVariables
+     * @param array<mixed> $context
+     *
+     * @throws \Doctrine\ORM\Exception\ORMException
+     * @throws \Doctrine\ORM\OptimisticLockException
+     */
+    private function provideItem(array $uriVariables, array $context): ?PlaceSearchItem
+    {
+        /** @var Event $event */
+        if(empty($event = $this->placeRepository->find($uriVariables['id']))){
+            throw new NotFoundHttpException('event not found');
+        }
+        return $this->objectMapper->map($event, PlaceSearchItem::class);
+    }
+}

+ 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) {

+ 34 - 0
src/Validator/Constraints/LessThanField.php

@@ -0,0 +1,34 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Validator\Constraints;
+
+use Attribute;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Attribute\HasNamedArguments;
+
+#[Attribute(Attribute::TARGET_CLASS)]
+class LessThanField extends Constraint
+{
+    public string $message = 'The value of "{{ field }}" must be less than "{{ comparedTo }}"';
+
+    #[HasNamedArguments]
+    public function __construct(
+        public string $field,
+        public string $comparedTo,
+        array $groups = null,
+        mixed $payload = null,
+    ) {
+        parent::__construct([], $groups, $payload);
+    }
+
+    public function getTargets(): string
+    {
+        return self::CLASS_CONSTRAINT;
+    }
+
+    public function validatedBy(): string
+    {
+        return static::class . 'Validator';
+    }
+}

+ 42 - 0
src/Validator/Constraints/LessThanFieldValidator.php

@@ -0,0 +1,42 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Validator\Constraints;
+
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+class LessThanFieldValidator extends ConstraintValidator
+{
+    public function validate(mixed $object, Constraint $constraint): void
+    {
+        if (!$constraint instanceof LessThanField) {
+            throw new UnexpectedTypeException($constraint, LessThanField::class);
+        }
+
+        if (!is_object($object)) {
+            return; // Only validate objects
+        }
+
+        $fieldValue = $object->{$constraint->field} ?? null;
+        $comparedValue = $object->{$constraint->comparedTo} ?? null;
+
+        if ($fieldValue === null || $comparedValue === null) {
+            return; // Skip if either value is null
+        }
+
+        if (!$fieldValue instanceof \DateTimeInterface || !$comparedValue instanceof \DateTimeInterface) {
+            return;
+        }
+
+        if ($fieldValue->getTimestamp() >= $comparedValue->getTimestamp()) {
+            $this->context
+                ->buildViolation($constraint->message)
+                ->setParameter('{{ field }}', $constraint->field)
+                ->setParameter('{{ comparedTo }}', $constraint->comparedTo)
+                ->atPath($constraint->field)
+                ->addViolation();
+        }
+    }
+}

+ 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');
     }
 
     /**