Ver código fonte

Merge branch 'release/2.6'

Vincent 2 meses atrás
pai
commit
f5fef08b30
100 arquivos alterados com 3076 adições e 234 exclusões
  1. 13 0
      .env
  2. 293 0
      .junie/guidelines.md
  3. 1 0
      Dockerfile
  4. 31 29
      composer.json
  5. 2 0
      config/opentalent/enum.yaml
  6. 63 48
      config/opentalent/products.yaml
  7. 10 2
      config/packages/api_platform.yaml
  8. 6 6
      config/packages/docker/messenger.yaml
  9. 4 2
      config/packages/doctrine.yaml
  10. 12 6
      config/packages/liip_imagine.yaml
  11. 5 1
      config/packages/messenger.yaml
  12. 3 0
      config/packages/property_info.yaml
  13. 3 0
      config/packages/security.yaml
  14. 3 0
      config/routes/dh_auditor.yaml
  15. 12 0
      config/routes/nelmio_api_doc.yaml
  16. 3 0
      config/secrets/docker/docker.DATABASE_DOLIBARR_URL.76f96a.php
  17. 1 0
      config/secrets/docker/docker.list.php
  18. 3 0
      config/secrets/prod/prod.DATABASE_DOLIBARR_URL.76f96a.php
  19. 1 0
      config/secrets/prod/prod.list.php
  20. 3 0
      config/secrets/staging/staging.DATABASE_DOLIBARR_URL.76f96a.php
  21. 1 0
      config/secrets/staging/staging.list.php
  22. 3 0
      config/secrets/test/test.DATABASE_DOLIBARR_URL.76f96a.php
  23. 1 0
      config/secrets/test/test.list.php
  24. 3 2
      config/services.yaml
  25. 9 1
      env/.env.docker
  26. 12 4
      env/.env.prod
  27. 9 1
      env/.env.staging
  28. 12 4
      env/.env.test
  29. 12 4
      env/.env.test1
  30. 12 4
      env/.env.test2
  31. 12 4
      env/.env.test3
  32. 12 4
      env/.env.test4
  33. 12 4
      env/.env.test5
  34. 12 4
      env/.env.test6
  35. 12 4
      env/.env.test7
  36. 12 4
      env/.env.test8
  37. 12 4
      env/.env.test9
  38. BIN
      public/images/facebook.jpg
  39. BIN
      public/images/linkedin.jpg
  40. BIN
      public/images/youtube.jpg
  41. 37 0
      src/ApiResources/Core/EventCategory.php
  42. 5 0
      src/ApiResources/Core/File/Image.php
  43. 164 0
      src/ApiResources/Freemium/FreemiumEvent.php
  44. 136 0
      src/ApiResources/Freemium/FreemiumOrganization.php
  45. 62 0
      src/ApiResources/Freemium/FreemiumPlace.php
  46. 5 8
      src/ApiResources/Organization/OrganizationCreationRequest.php
  47. 7 14
      src/ApiResources/Organization/OrganizationMemberCreationRequest.php
  48. 5 0
      src/ApiResources/Organization/Subdomain/SubdomainAvailability.php
  49. 48 0
      src/ApiResources/Search/PlaceSearchItem.php
  50. 354 0
      src/ApiResources/Shop/NewStructureArtistPremiumTrialRequest.php
  51. 9 0
      src/ApiResources/Shop/ShopRequestData.php
  52. 14 0
      src/ApiResources/Utils/GpsCoordinate.php
  53. 2 4
      src/Doctrine/Access/AdditionalExtension/DateTimeConstraintExtensionAdditional.php
  54. 0 5
      src/Doctrine/Booking/CurrentCoursesExtension.php
  55. 41 0
      src/Doctrine/Booking/CurrentEventsExtension.php
  56. 41 0
      src/Doctrine/Place/CurrentPlaceExtension.php
  57. 126 0
      src/Entity/Booking/Event.php
  58. 31 1
      src/Entity/Booking/EventGender.php
  59. 1 1
      src/Entity/Core/Categories.php
  60. 15 0
      src/Entity/Core/Familly.php
  61. 15 0
      src/Entity/Core/Gender.php
  62. 15 0
      src/Entity/Core/Subfamilly.php
  63. 4 0
      src/Entity/Organization/Organization.php
  64. 37 0
      src/Entity/Organization/Traits/OrganizationComputedTraits.php
  65. 13 8
      src/Entity/Organization/TypeOfPractice.php
  66. 16 1
      src/Entity/Place/AbstractPlace.php
  67. 2 1
      src/Entity/Place/Place.php
  68. 119 0
      src/Entity/Shop/ShopRequest.php
  69. 22 0
      src/Enum/Access/AccessIdsEnum.php
  70. 19 0
      src/Enum/Booking/EventGenderTypeEnum.php
  71. 18 0
      src/Enum/Booking/PricingEventEnum.php
  72. 2 0
      src/Enum/Cotisation/CategoryTypeOfPracticeEnum.php
  73. 1 1
      src/Enum/Organization/OrganizationIdsEnum.php
  74. 19 0
      src/Enum/Organization/PrincipalTypeShortListEnum.php
  75. 1 0
      src/Enum/Organization/SettingsProductEnum.php
  76. 28 0
      src/Enum/Shop/ShopRequestStatus.php
  77. 22 0
      src/Enum/Shop/ShopRequestType.php
  78. 2 10
      src/EventListener/OnKernelRequestPreRead.php
  79. 2 2
      src/Filter/ApiPlatform/Utils/FindInSetFilter.php
  80. 2 2
      src/Filter/ApiPlatform/Utils/InFilter.php
  81. 36 0
      src/Message/Handler/Shop/NewStructureArtistPremiumTrialHandler.php
  82. 42 0
      src/Message/Message/Shop/NewStructureArtistPremiumTrial.php
  83. 1 1
      src/Repository/Access/AccessRepository.php
  84. 17 0
      src/Repository/Place/PlaceRepository.php
  85. 1 1
      src/Security/Voter/EntityVoter/AbstractEntityVoter.php
  86. 9 7
      src/Security/Voter/ModuleVoter.php
  87. 0 1
      src/Service/ApiLegacy/ApiLegacyRequestService.php
  88. 138 0
      src/Service/ApiResourceBuilder/Freemium/EventMappingBuilder.php
  89. 173 0
      src/Service/ApiResourceBuilder/Freemium/OrganizationMappingBuilder.php
  90. 18 2
      src/Service/Doctrine/FiltersConfigurationService.php
  91. 125 0
      src/Service/Dolibarr/DolibarrApiService.php
  92. 140 0
      src/Service/Dolibarr/DolibarrUtils.php
  93. 20 16
      src/Service/Export/Encoder/PdfEncoder.php
  94. 5 4
      src/Service/Export/LicenceCmfExporter.php
  95. 1 1
      src/Service/File/Storage/ApiLegacyStorage.php
  96. 3 1
      src/Service/Mailer/Builder/AbstractBuilder.php
  97. 58 0
      src/Service/Mailer/Builder/Shop/NewStructureArtistPremium/ConfirmationToRepresentativeBuilder.php
  98. 58 0
      src/Service/Mailer/Builder/Shop/NewStructureArtistPremium/NotificationToSalesAdminBuilder.php
  99. 57 0
      src/Service/Mailer/Builder/Shop/TokenValidationBuilder.php
  100. 82 0
      src/Service/Mailer/Model/Shop/NewStructureArtistPremium/ConfirmationToRepresentativeModel.php

+ 13 - 0
.env

@@ -23,6 +23,7 @@ APP_SECRET=6a76497c8658bb23e2236f97a2627df3
 DATABASE_URL=xxx
 DATABASE_ADMINASSOS_URL=xxx
 DATABASE_AUDIT_URL=xxx
+DATABASE_DOLIBARR_URL=xxx
 DOLIBARR_API_TOKEN=xxx
 MERCURE_JWT_SECRET=xxx
 ###< secret values ###
@@ -31,6 +32,10 @@ MERCURE_JWT_SECRET=xxx
 CORS_ALLOW_ORIGIN=^https?://(localhost|127\.0\.0\.1)(:[0-9]+)$
 ###< nelmio/cors-bundle ###
 
+### Logiciel
+ADMIN_BASE_URL=https://local.admin.opentalent.fr
+###
+
 ###> url v2 ###
 PUBLIC_API_BASE_URL=https://local.ap2i.opentalent.fr
 ###
@@ -115,3 +120,11 @@ BLACKFIRE_CLIENT_TOKEN=8cfbeb263d044da9678dc2612531504da3790c308da7448e35724a5da
 BLACKFIRE_SERVER_ID=1171e53b-459b-41da-a292-80ff68cee8c2
 BLACKFIRE_SERVER_TOKEN=dbd1cfbea015fe83cccfc189a36ca3c16f3a1b43b94f50032a15e41e53548e8b
 ###< BlackFire configuration ###
+
+### Lien vers la FAQ
+FAQ_URL=https://ressources.opentalent.fr/space/FAQ/2495122/Artist+Standard+et+Premium+-+Prise+en+main
+###
+
+### Site logiciels
+SOFTWARE_WEBSITE_URL=https://local.logiciels.opentalent.fr
+###

+ 293 - 0
.junie/guidelines.md

@@ -0,0 +1,293 @@
+# AP2I Project Guidelines
+
+## Project Overview
+
+AP2I is a Symfony 7.3-based API application that is part of the OpenTalent ecosystem. 
+It's a comprehensive API platform built with modern PHP (8.2+) and leverages API Platform 4.1 for REST/GraphQL API capabilities.
+
+### Key Technologies
+- **Framework**: Symfony 7.3
+- **API Platform**: 4.1 (REST/GraphQL APIs)
+- **PHP**: 8.2+ (strict requirement)
+- **ORM**: Doctrine 3.3 with migrations
+- **Authentication**: JWT (Lexik JWT Authentication Bundle)
+- **Database**: Uses Doctrine DBAL 3.9 and a mariadb 10.4.26 DB
+- **Messaging**: Symfony Messenger for async processing
+- **Testing**: PHPUnit 9.6
+- **Code Quality**: PHPStan, PHP CS Fixer
+
+### Business Domains
+The application handles multiple business domains including:
+- Organization management
+- Education systems
+- Billing and payments
+- User profiles and access control
+- Network management
+- Booking systems
+- Export functionality
+- Shop/e-commerce features
+
+## Project Structure
+
+```
+src/
+├── Message/           # Symfony Messenger messages and handlers
+├── DataFixtures/      # Database fixtures for testing/development
+├── Validator/         # Custom validation logic
+├── Enum/              # Domain-specific enumerations
+├── Service/           # Business logic services
+└── ...               # Other domain-specific directories
+
+tests/
+├── Fixture/          # Test data fixtures and factories
+├── Application/      # Application-level integration tests
+├── Unit/            # Unit tests organized by domain
+└── ...
+
+config/               # Symfony configuration
+public/              # Web root directory
+templates/           # Twig templates
+migrations/          # Doctrine database migrations
+```
+
+## Development Guidelines
+
+### Testing
+- **Always run tests** before submitting changes to ensure correctness
+- The project uses PHPUnit for testing with comprehensive unit and application tests
+- Tests are organized by domain to mirror the application structure
+- Do not modify the SUT, except for converting private methods into protected ones, or because there is an obvious error in it
+- Take `tests/Unit/Service/Typo3/Typo3ServiceTest.php` as the reference example for writing service tests
+
+#### Test Class Structure
+Each test class should follow this structure:
+
+```php
+<?php
+
+namespace App\Tests\Unit\Service\MyDomain;
+
+use App\Service\MyDomain\MyService;
+use PHPUnit\Framework\TestCase;
+use SomeDependency\Interface;
+use PHPUnit\Framework\MockObject\MockObject;
+
+// Create testable class for protected methods access
+class TestableMyService extends MyService
+{
+    public function protectedMethod(): mixed
+    {
+        return parent::protectedMethod();
+    }
+}
+
+class MyServiceTest extends TestCase
+{
+    private Interface|MockObject $dependency;
+
+    public function setUp(): void
+    {
+        // Mock all dependencies in setUp
+        $this->dependency = $this->getMockBuilder(Interface::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+    }
+
+    private function getMyServiceMockFor(string $methodName): TestableMyService|MockObject
+    {
+        return $this->getMockBuilder(TestableMyService::class)
+            ->setConstructorArgs([$this->dependency])
+            ->setMethodsExcept([$methodName])
+            ->getMock();   
+    }
+}
+```
+
+#### Mocking Strategy
+- **Use `setMethodsExcept()`** to mock all methods except the one being tested (ignore deprecation warnings)
+- **Create a dedicated mock factory method** following the pattern `getMyClassMockFor(string $methodName)`
+- **Mock factory method should:**
+  - Create the SUT mock using `getMockBuilder()`
+  - Inject dependency mocks (created in `setUp`)
+  - Define methods to exclude from mocking via `setMethodsExcept()`
+  - Return the configured mock object
+
+#### Method Mocking Rules
+- **Only mock methods that are expected to be called** during test execution
+- **Exception:** Use `expects(self::never())` when verifying a method should NOT be called
+- **Mock each method called** in the currently tested method, including SUT methods
+- **Do not mock methods** that are not involved in the test scenario
+
+Example:
+```php
+public function testClearSiteCache(): void
+{
+    $typo3Service = $this->getTypo3ServiceMockFor('clearSiteCache');
+    
+    $response = $this->getMockBuilder(ResponseInterface::class)
+        ->disableOriginalConstructor()
+        ->getMock();
+        
+    $typo3Service->expects(self::once())
+        ->method('sendCommand')
+        ->with('/otadmin/site/clear-cache', ['organization-id' => 1])
+        ->willReturn($response);
+
+    $typo3Service->clearSiteCache(1);
+}
+```
+
+#### Visibility Issues
+- **For protected methods:** Create an intermediate testable class (see `TestableTypo3Service` example)
+- **For private methods in SUT:** Change visibility to protected to enable testing
+- **Testable class pattern:**
+
+```php
+class TestableMyService extends MyService
+{
+    public function protectedMethodToTest(string $param): ResponseInterface
+    {
+        return parent::protectedMethodToTest($param);
+    }
+}
+```
+
+#### Test Naming Convention
+- **Primary test** covering most common usage: `testMethod` (where "Method" is the actual method name)
+- **Variant tests** append specific situation:
+  - `testMethodWhenNoInput`
+  - `testMethodWithInvalidContext`
+  - `testMethodWithSpecialCondition`
+
+#### Test Documentation
+- **Use @see annotations** to reference the tested method:
+```php
+/**
+ * @see MyService::clearSiteCache()
+ */
+public function testClearSiteCache(): void
+```
+
+#### Test Structure Best Practices
+- **One test per execution branch/path**
+- **Use assertions** when the method should return a result
+- **Use expectations** when the method should call other methods
+- **Use `expects(self::never())`** when verifying a method should NOT be called
+- **Cover edge cases and error scenarios**
+- **Test method variants** (e.g., with/without optional parameters)
+
+#### Dependencies and Isolation
+- **All dependencies must be mocked** in `setUp` method
+- **Tests must be isolated and independent**
+- **Use `disableOriginalConstructor()`** for mock builders
+- **Mock return values** using `willReturn()` for expected behaviors
+
+#### Example Test Patterns
+
+**Simple method expectation:**
+```php
+public function testCreateSite(): void
+{
+    $typo3Service = $this->getTypo3ServiceMockFor('createSite');
+    
+    $response = $this->getMockBuilder(ResponseInterface::class)
+        ->disableOriginalConstructor()->getMock();
+        
+    $typo3Service->expects(self::once())
+        ->method('sendCommand')
+        ->with('/otadmin/site/create', ['organization-id' => 1])
+        ->willReturn($response);
+
+    $typo3Service->createSite(1);
+}
+```
+
+**Method with conditional parameters:**
+```php
+public function testSetSiteDomainWithRedirection(): void
+{
+    $typo3Service = $this->getTypo3ServiceMockFor('setSiteDomain');
+    
+    $response = $this->getMockBuilder(ResponseInterface::class)
+        ->disableOriginalConstructor()->getMock();
+        
+    $typo3Service->expects(self::once())
+        ->method('sendCommand')
+        ->with('/otadmin/site/set-domain', [
+            'organization-id' => 1, 
+            'domain' => 'new-domain', 
+            'redirect' => 1
+        ])
+        ->willReturn($response);
+
+    $typo3Service->setSiteDomain(1, 'new-domain', true);
+}
+```
+
+#### Running Tests
+```bash
+# Run all unit tests
+docker exec ap2i php -d memory_limit=-1 vendor/phpunit/phpunit/phpunit --testsuite unit --configuration phpunit.xml.dist
+
+# Run application tests (adjust testsuite as needed)
+docker exec ap2i vendor/bin/phpunit --configuration phpunit.xml.dist
+
+# Run specific test class
+docker exec ap2i vendor/bin/phpunit tests/Unit/Service/MyDomain/MyServiceTest.php
+
+# Run with coverage (if configured)
+docker exec ap2i vendor/bin/phpunit --coverage-html coverage/
+```
+
+### Code Quality
+The project enforces strict code quality standards:
+
+#### PHPStan (Static Analysis)
+```bash
+# Run analysis
+docker exec ap2i vendor/bin/phpstan analyse
+
+# Clear cache if needed
+docker exec ap2i vendor/bin/phpstan clear-result-cache
+```
+
+#### PHP CS Fixer (Code Style)
+```bash
+# Check code style
+docker exec ap2i php vendor/bin/php-cs-fixer check --config=.php-cs-fixer.dist.php
+
+# Fix code style automatically
+docker exec ap2i php vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php
+```
+
+### Messenger (Async Processing)
+The application uses Symfony Messenger for async processing:
+
+```bash
+# Start consuming messages
+docker exec ap2i php bin/console messenger:consume async
+
+# Setup transports if needed
+docker exec ap2i php bin/console messenger:setup-transports
+```
+
+### Build Process
+- No special build process required beyond standard Symfony practices
+- Run `docker exec ap2i composer install` for dependencies
+- Run database migrations: `docker exec ap2i php bin/console doctrine:migrations:migrate`
+- Clear cache: `docker exec ap2i php bin/console cache:clear`
+
+### Code Style Guidelines
+- Follow PSR-12 coding standards (enforced by PHP CS Fixer)
+- Use strict typing where possible (PHP 8.2+ features encouraged)
+- Follow Symfony best practices for service organization
+- Organize code by business domain
+- Write comprehensive tests for all business logic
+- Use PHPStan level checks for type safety
+
+### Important Notes
+- This is a proprietary project (license: proprietary)
+- Uses custom OpenTalent packages and private GitLab repositories
+- Requires PHP 8.2 minimum
+- Database changes must be done via Doctrine migrations
+- Always run the full test suite before submitting changes

+ 1 - 0
Dockerfile

@@ -1,3 +1,4 @@
+# Dockerfile utilisé pour la CI
 FROM php:8.2-fpm
 
 # Installation des dépendances système

+ 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": [

+ 2 - 0
config/opentalent/enum.yaml

@@ -55,6 +55,7 @@ parameters:
           organization_legal: 'App\Enum\Organization\LegalEnum'
           organization_opca: 'App\Enum\Organization\OpcaEnum'
           organization_principal_type: 'App\Enum\Organization\PrincipalTypeEnum'
+          organization_principal_type_short_list: 'App\Enum\Organization\PrincipalTypeShortListEnum'
           organization_school_cat: 'App\Enum\Organization\SchoolCategoryEnum'
           organization_type_establishment_detail: 'App\Enum\Organization\TypeEstablishmentDetailEnum'
           organization_type_establishment: 'App\Enum\Organization\TypeEstablishmentEnum'
@@ -118,6 +119,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'

+ 63 - 48
config/opentalent/products.yaml

@@ -1,38 +1,48 @@
 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
+          - EventGender
+          - TypeOfPractice
+      Common:
+        resources:
+          - Notification
+          - NotificationUser
+          - 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 +64,7 @@ parameters:
           - ROLE_GENERAL_CONFIG
 
       Users:
-        entities:
+        resources:
           - Access
           - Commission
           - File
@@ -71,25 +81,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 +107,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 +147,7 @@ parameters:
             - ROLE_EVENTS
 
       Courses:
-          entities:
+          resources:
             - Course
             - Work
             - WorkByUser
@@ -146,28 +156,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 +188,7 @@ parameters:
             - ROLE_EQUIPMENTS
 
       PedagogicsAdministation:
-          entities:
+          resources:
             - EducationTeacher
             - EducationStudent
             - EducationCurriculum
@@ -193,18 +203,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 +231,11 @@ parameters:
             - ROLE_BILLINGS_SEIZURE
 
       Pes:
-          entities:
+          resources:
             - Pes
 
       IEL:
-          entities:
+          resources:
             - AccessWish
             - AccessFamilyWish
             - AccessTmp
@@ -237,61 +247,67 @@ 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:
+        resources: ~
         roles:
           - ROLE_BASICOMPTA
 
   opentalent.products:
+      freemium:
+        modules:
+          - Freemium
+          - Core
       artist:
         modules:
           - Core
+          - Common
           - Users
           - Events
           - GeneralConfig
@@ -335,5 +351,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:
         graphiql:
             enabled: false

+ 6 - 6
config/packages/docker/messenger.yaml

@@ -1,9 +1,9 @@
 # Désactive le fonctionnement asynchrone de messenger en mode dev
 # > commenter pour tester avec un fonctionnement asynchrone
 #   (dans ce cas, la commande `messenger:consume async` doit être en cours d'exécution)
-#framework:
-#    messenger:
-#        transports:
-#            # https://symfony.com/doc/current/messenger.html#transport-configuration
-#            async: 'sync://'
-#            failed: 'sync://'
+framework:
+    messenger:
+        transports:
+            # https://symfony.com/doc/current/messenger.html#transport-configuration
+            async: 'sync://'
+            failed: 'sync://'

+ 4 - 2
config/packages/doctrine.yaml

@@ -17,10 +17,12 @@ doctrine:
 
             adminassos:
                 url: '%env(resolve:DATABASE_ADMINASSOS_URL)%'
+                server_version: '5.7'
 
-                # IMPORTANT: You MUST configure your server version,
-                # either here or in the DATABASE_URL env var (see .env file)
+            dolibarr:
+                url: '%env(resolve:DATABASE_DOLIBARR_URL)%'
                 server_version: '5.7'
+
         types:
             uuid: Ramsey\Uuid\Doctrine\UuidType
 

+ 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

+ 5 - 1
config/packages/messenger.yaml

@@ -5,7 +5,10 @@ framework:
 
         transports:
             # https://symfony.com/doc/current/messenger.html#transport-configuration
-            async: '%env(MESSENGER_TRANSPORT_DSN)%'
+            async:
+                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
+                retry_strategy:
+                    max_retries: 0
             failed: '%env(MESSENGER_TRANSPORT_DSN)%?queue_name=failed'
             sync: 'sync://'
 
@@ -18,3 +21,4 @@ framework:
             'App\Message\Message\Typo3\Typo3Undelete': async
             'App\Message\Message\OrganizationCreation': async
             'App\Message\Message\OrganizationDeletion': async
+

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

+ 3 - 0
config/secrets/docker/docker.DATABASE_DOLIBARR_URL.76f96a.php

@@ -0,0 +1,3 @@
+<?php // docker.DATABASE_DOLIBARR_URL.76f96a on Thu, 19 Jun 2025 09:06:57 +0000
+
+return "gw\xD4\x05\x0E\xED\xBD2\xB5\xE7I\x97\x02\x237\x1DX\xBA\x5C\x04\x5BePi\xB3dl\xFD\xD0\x273J\x24\xE3\xC7Y\x28\xC9\xDE\xD0K\xB0\xC7\xF2\xD1E\x1E\xB9\xF2\xC0\xDB25\x5D\x89S\x3A\x02\x7C\xA6\xC0\x3B\xA6\xA3\xD4\x01I\xA8\x16G\xB4l\x8DzE\xD5\xFCuq\x84\xE0n\x241q\x18\x3C\xAA\xDC\x84\xB0\x17\x0B\xF5\xD1o\xF9\x02\x8C\xA6\x9CJ\x05\xF1\xA4\x86R\xBD\xBB\xC3i\x935\x1E\x18E\x176\x18\x91\x40\xEB\xD1\x19v\x24\xF0\x17\x23h\x18\xC8\xFB\xD7\x60yY\xEA\x8F\x04\x8F\xB8\xE9Z\xDB2a\xEB5\xAE\xF7\x7C\x92\xA3\xFF";

+ 1 - 0
config/secrets/docker/docker.list.php

@@ -3,6 +3,7 @@
 return [
     'DATABASE_ADMINASSOS_URL' => null,
     'DATABASE_AUDIT_URL' => null,
+    'DATABASE_DOLIBARR_URL' => null,
     'DATABASE_URL' => null,
     'DOLIBARR_API_TOKEN' => null,
     'MERCURE_JWT_SECRET' => null,

+ 3 - 0
config/secrets/prod/prod.DATABASE_DOLIBARR_URL.76f96a.php

@@ -0,0 +1,3 @@
+<?php // prod.DATABASE_DOLIBARR_URL.76f96a on Thu, 19 Jun 2025 09:58:07 +0000
+
+return "Em\x5B\xEDT\xDF\x40\x12E\xC9g\x3D\xAAZ\x9CfW\xF5\x83\x2Ac\xD3\x95Q\xC7\xAD\x83k1\xEF\xFD4s\x16\xAC\x0F\x1E\xE45l\xA6\x04q\x7F\xD2\xA0\x85\xCA\x3F\x80\x04\x0EMlI9\x92\xC9\xE4G\x03\x14G\xA1\xD0\x92\x8C\x08\x9Cur\x97\xBEg\x82\xE2\xE6\xD3\xD8\xA8\xBA\xDF\x1A\xFBYL\x25\x1C\x83\xC7D\x1D1\x23Wy\x98\xB4\xBCn\xD7\xA5V\xBC\x7D\xC3\x88F\x0D\xF6\x82\xB17\x8F\xEFi\x0E\xC1\xA5W\x3D\xE7\x90\x20A6\xB5\x01\x2C\xE5\xB2\xEA\xF5U\xBF\x2A\xF3\xDA\xAF_\x99\xC6sv\xF32\xB0H\x05\x84u\x3F\x60\xF8\xF9\x7C";

+ 1 - 0
config/secrets/prod/prod.list.php

@@ -3,6 +3,7 @@
 return [
     'DATABASE_ADMINASSOS_URL' => null,
     'DATABASE_AUDIT_URL' => null,
+    'DATABASE_DOLIBARR_URL' => null,
     'DATABASE_URL' => null,
     'DOLIBARR_API_TOKEN' => null,
     'MERCURE_JWT_SECRET' => null,

+ 3 - 0
config/secrets/staging/staging.DATABASE_DOLIBARR_URL.76f96a.php

@@ -0,0 +1,3 @@
+<?php // staging.DATABASE_DOLIBARR_URL.76f96a on Thu, 19 Jun 2025 09:58:32 +0000
+
+return "V\xE2\x8C\xCA\x02\xA6\x8A5\xE3U\x0BbW\xA9\xD8c\x22L\xA7\xE5\xC54\x02\x94\xC7G\xA1D\x26\x0DH\x2B\x20\x3D\x3C\xAC\x5C\xC1\x5E\x21\xF1\x8F.\xBBm\x9A\xC6\x3D";

+ 1 - 0
config/secrets/staging/staging.list.php

@@ -3,6 +3,7 @@
 return [
     'DATABASE_ADMINASSOS_URL' => null,
     'DATABASE_AUDIT_URL' => null,
+    'DATABASE_DOLIBARR_URL' => null,
     'DATABASE_URL' => null,
     'DOLIBARR_API_TOKEN' => null,
 ];

+ 3 - 0
config/secrets/test/test.DATABASE_DOLIBARR_URL.76f96a.php

@@ -0,0 +1,3 @@
+<?php // test.DATABASE_DOLIBARR_URL.76f96a on Thu, 19 Jun 2025 09:58:21 +0000
+
+return "d\xD8\xF9\xFD\x06\x80\xF8\x209\x1E\xAD\xAD\xCC\xEE\xBF\xD6\xD3\x25\xC2~\xC0\xE5r\xCE\xE7\xED-\x10\xF7\xC3\xF0\x29b\xE5Hw-q\x27\x88\xF1\x9A.\xDF\x04Y\x87\xD3x\xCApN6\xD5\xC3\x9E9\x89\x40\x3Et\xA1\xF6\xEE\x8D\xBB\xC1c\xD4\x9E\xB0\x0E\xCA\xBC\xC3\xC8\x1D\x2F\x2A~\x0D\xBE\x93\x12u\x23\xB0\x8F\x04Yds\xACx\xDC\x23\xBErs\xA8\xB8o\xB3\x8D\xE9\x0D\x26\xEF\xCE\xC5\xA0\x3E\xAB\xB8Vk\x9F\x11\x9D\xA4\x7CSG\x1A\xA7w\xAF\xDEO\x0D\xFEqY\xD8\xD1\xDD\xA9\x85T\x8A\xDF\x12\xF0\xDF\x8Fd\x15\x40\xE4\xE2\x9B\xC3\x27\xCF\xB2";

+ 1 - 0
config/secrets/test/test.list.php

@@ -3,6 +3,7 @@
 return [
     'DATABASE_ADMINASSOS_URL' => null,
     'DATABASE_AUDIT_URL' => null,
+    'DATABASE_DOLIBARR_URL' => null,
     'DATABASE_URL' => null,
     'DOLIBARR_API_TOKEN' => null,
     'MERCURE_JWT_SECRET' => null,

+ 3 - 2
config/services.yaml

@@ -25,8 +25,11 @@ services:
             $publicLegacyBaseUrl: '%env(PUBLIC_API_LEG_BASE_URL)%'
             $baseUrl: '%env(API_BASE_URL)%'
             $publicBaseUrl: '%env(PUBLIC_API_BASE_URL)%'
+            $adminBaseUrl: '%env(ADMIN_BASE_URL)%'
+            $softwareWebsiteUrl: '%env(SOFTWARE_WEBSITE_URL)%'
             $opentalentMailReport: 'mail.report@opentalent.fr'
             $fileStorageDir: '%kernel.project_dir%/var/files/storage'
+            $faqUrl: '%env(FAQ_URL)%'
 
     # makes classes in src/ available to be used as services
     # this creates a service per class whose id is the fully-qualified class name
@@ -132,5 +135,3 @@ services:
         arguments:
             - '@doctrine.orm.command.entity_manager_provider'
         tags: ['console.command']
-
-

+ 9 - 1
env/.env.docker

@@ -5,9 +5,13 @@ APP_SECRET=211cede3dc4b162da3ec2fbdcd905070
 ###< symfony/framework-bundle ###
 
 ###> nelmio/cors-bundle ###
-CORS_ALLOW_ORIGIN=^https?:\/\/(localhost|127\.0\.0\.1|(local.(admin|app|app|frames|agenda|maestro).opentalent.fr))(:[0-9]+)?$
+CORS_ALLOW_ORIGIN=^https?:\/\/(localhost|127\.0\.0\.1|(local.(admin|app|app|frames|agenda|maestro|logiciels).opentalent.fr))(:[0-9]+)?$
 ###< nelmio/cors-bundle ###
 
+### Logiciel
+ADMIN_BASE_URL=https://local.admin.opentalent.fr
+###
+
 ###> api v1 ###
 API_LEG_BASE_URL=http://nginx/
 PUBLIC_API_LEG_BASE_URL=https://local.api.opentalent.fr
@@ -22,6 +26,10 @@ PUBLIC_API_BASE_URL=https://local.ap2i.opentalent.fr
 TYPO3_BASE_URI=http://docker.sub.opentalent.fr
 ###< typo3 client ###
 
+### Site logiciels
+SOFTWARE_WEBSITE_URL=https://local.logiciels.opentalent.fr
+###
+
 ###> symfony/mercure-bundle ###
 # See https://symfony.com/doc/current/mercure.html#configuration
 # The URL of the Mercure hub, used by the app to publish updates (can be a local URL)

+ 12 - 4
env/.env.prod

@@ -1,20 +1,28 @@
 ###> doctrine/doctrine-bundle ###
 APP_ENV=prod
 
+### Logiciel
+ADMIN_BASE_URL=https://admin.opentalent.fr
+###
+
 ###> api v1 ###
-API_LEG_BASE_URL=https://api.opentalent.fr/api
-PUBLIC_API_LEG_BASE_URL=https://api.opentalent.fr/api
+API_LEG_BASE_URL=https://api.opentalent.fr
+PUBLIC_API_LEG_BASE_URL=https://api.opentalent.fr
 ###
 
 ###> url v2 ###
-API_BASE_URL=https://ap2i.opentalent.fr/api
-PUBLIC_API_BASE_URL=https://ap2i.opentalent.fr/api
+API_BASE_URL=https://ap2i.opentalent.fr
+PUBLIC_API_BASE_URL=https://ap2i.opentalent.fr
 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=http://ohcluses.opentalent.fr
 ###< typo3 client ###
 
+### Site logiciels
+SOFTWARE_WEBSITE_URL=https://logiciels.opentalent.fr
+###
+
 ###> dolibarr client ###
 DOLIBARR_API_BASE_URI=https://prod-erp.2iopenservice.com/api/index.php/
 ###< dolibarr client ###

+ 9 - 1
env/.env.staging

@@ -8,13 +8,17 @@ MERCURE_JWT_SECRET='TXsLVKnU4Ew4oyH4qcQO81CuSOVTbj58W42fDTIzIZPwpPCaGu2EvIL3DbtD
 CORS_ALLOW_ORIGIN=^$
 ###< nelmio/cors-bundle ###
 
+### Logiciel
+ADMIN_BASE_URL=https://none
+###
+
 ####> api v1 ###
 API_LEG_BASE_URL=https://none
 PUBLIC_API_LEG_BASE_URL=https://none
 ###< api v1 ###
 
 ###> api v2 ###
-API_BASE_URL=https://ap2i.ci.opentalent.fr/api
+API_BASE_URL=https://ap2i.ci.opentalent.fr
 PUBLIC_API_BASE_URL=https://ap2i.ci.opentalent.fr
 ###< api v2 ###
 
@@ -27,6 +31,10 @@ ELASTICSEARCH_PORT=9200
 TYPO3_BASE_URI=https://none
 ###< typo3 client ###
 
+### Site logiciels
+SOFTWARE_WEBSITE_URL=none
+###
+
 ### Internal requests (@see doc/internal_requests.md)
 INTERNAL_FILES_DOWNLOAD_URI=https://none
 ###

+ 12 - 4
env/.env.test

@@ -3,20 +3,28 @@ APP_ENV=test
 APP_DEBUG=1
 ###< symfony/framework-bundle ###
 
+### Logiciel
+ADMIN_BASE_URL=https://admin.test.opentalent.fr
+###
+
 ###> api v1 ###
-API_LEG_BASE_URL=https://api.test.opentalent.fr/api
-PUBLIC_API_LEG_BASE_URL=https://api.test.opentalent.fr/api
+API_LEG_BASE_URL=https://api.test.opentalent.fr
+PUBLIC_API_LEG_BASE_URL=https://api.test.opentalent.fr
 ###< api v1 ###
 
 ###> api v2 ###
-API_BASE_URL=https://ap2i.test.opentalent.fr/api
-PUBLIC_API_BASE_URL=https://ap2i.test.opentalent.fr/api
+API_BASE_URL=https://ap2i.test.opentalent.fr
+PUBLIC_API_BASE_URL=https://ap2i.test.opentalent.fr
 ###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=https://sub.test.opentalent.fr
 ###< typo3 client ###
 
+### Site logiciels
+SOFTWARE_WEBSITE_URL=https://logiciels.test.opentalent.fr
+###
+
 ###> symfony/mercure-bundle ###
 # See https://symfony.com/doc/current/mercure.html#configuration
 # The URL of the Mercure hub, used by the app to publish updates (can be a local URL)

+ 12 - 4
env/.env.test1

@@ -3,20 +3,28 @@ APP_ENV=test
 APP_DEBUG=1
 ###< symfony/framework-bundle ###
 
+### Logiciel
+ADMIN_BASE_URL=https://admin.test1.opentalent.fr
+###
+
 ###> api v1 ###
-API_LEG_BASE_URL=https://api.test1.opentalent.fr/api
-PUBLIC_API_LEG_BASE_URL=https://api.test1.opentalent.fr/api
+API_LEG_BASE_URL=https://api.test1.opentalent.fr
+PUBLIC_API_LEG_BASE_URL=https://api.test1.opentalent.fr
 ###< api v1 ###
 
 ###> api v2 ###
-API_BASE_URL=https://ap2i.test1.opentalent.fr/api
-PUBLIC_API_BASE_URL=https://ap2i.test1.opentalent.fr/api
+API_BASE_URL=https://ap2i.test1.opentalent.fr
+PUBLIC_API_BASE_URL=https://ap2i.test1.opentalent.fr
 ###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=https://sub.test1.opentalent.fr
 ###< typo3 client ###
 
+### Site logiciels
+SOFTWARE_WEBSITE_URL=https://logiciels.test1.opentalent.fr
+###
+
 ###> symfony/mercure-bundle ###
 # See https://symfony.com/doc/current/mercure.html#configuration
 # The URL of the Mercure hub, used by the app to publish updates (can be a local URL)

+ 12 - 4
env/.env.test2

@@ -3,20 +3,28 @@ APP_ENV=test
 APP_DEBUG=1
 ###< symfony/framework-bundle ###
 
+### Logiciel
+ADMIN_BASE_URL=https://admin.test2.opentalent.fr
+###
+
 ###> api v1 ###
-API_LEG_BASE_URL=https://api.test2.opentalent.fr/api
-PUBLIC_API_LEG_BASE_URL=https://api.test2.opentalent.fr/api
+API_LEG_BASE_URL=https://api.test2.opentalent.fr
+PUBLIC_API_LEG_BASE_URL=https://api.test2.opentalent.fr
 ###< api v1 ###
 
 ###> api v2 ###
-API_BASE_URL=https://ap2i.test2.opentalent.fr/api
-PUBLIC_API_BASE_URL=https://ap2i.test2.opentalent.fr/api
+API_BASE_URL=https://ap2i.test2.opentalent.fr
+PUBLIC_API_BASE_URL=https://ap2i.test2.opentalent.fr
 ###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=https://sub.test2.opentalent.fr
 ###< typo3 client ###
 
+### Site logiciels
+SOFTWARE_WEBSITE_URL=https://logiciels.test2.opentalent.fr
+###
+
 ###> symfony/mercure-bundle ###
 # See https://symfony.com/doc/current/mercure.html#configuration
 # The URL of the Mercure hub, used by the app to publish updates (can be a local URL)

+ 12 - 4
env/.env.test3

@@ -3,20 +3,28 @@ APP_ENV=test
 APP_DEBUG=1
 ###< symfony/framework-bundle ###
 
+### Logiciel
+ADMIN_BASE_URL=https://admin.test3.opentalent.fr
+###
+
 ###> api v1 ###
-API_LEG_BASE_URL=https://api.test3.opentalent.fr/api
-PUBLIC_API_LEG_BASE_URL=https://api.test3.opentalent.fr/api
+API_LEG_BASE_URL=https://api.test3.opentalent.fr
+PUBLIC_API_LEG_BASE_URL=https://api.test3.opentalent.fr
 ###< api v1 ###
 
 ###> api v2 ###
-API_BASE_URL=https://ap2i.test3.opentalent.fr/api
-PUBLIC_API_BASE_URL=https://ap2i.test3.opentalent.fr/api
+API_BASE_URL=https://ap2i.test3.opentalent.fr
+PUBLIC_API_BASE_URL=https://ap2i.test3.opentalent.fr
 ###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=https://sub.test3.opentalent.fr
 ###< typo3 client ###
 
+### Site logiciels
+SOFTWARE_WEBSITE_URL=https://logiciels.test3.opentalent.fr
+###
+
 ###> symfony/mercure-bundle ###
 # See https://symfony.com/doc/current/mercure.html#configuration
 # The URL of the Mercure hub, used by the app to publish updates (can be a local URL)

+ 12 - 4
env/.env.test4

@@ -3,20 +3,28 @@ APP_ENV=test
 APP_DEBUG=1
 ###< symfony/framework-bundle ###
 
+### Logiciel
+ADMIN_BASE_URL=https://admin.test4.opentalent.fr
+###
+
 ###> api v1 ###
-API_LEG_BASE_URL=https://api.test4.opentalent.fr/api
-PUBLIC_API_LEG_BASE_URL=https://api.test4.opentalent.fr/api
+API_LEG_BASE_URL=https://api.test4.opentalent.fr
+PUBLIC_API_LEG_BASE_URL=https://api.test4.opentalent.fr
 ###< api v1 ###
 
 ###> api v2 ###
-API_BASE_URL=https://ap2i.test4.opentalent.fr/api
-PUBLIC_API_BASE_URL=https://ap2i.test4.opentalent.fr/api
+API_BASE_URL=https://ap2i.test4.opentalent.fr
+PUBLIC_API_BASE_URL=https://ap2i.test4.opentalent.fr
 ###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=https://sub.test4.opentalent.fr
 ###< typo3 client ###
 
+### Site logiciels
+SOFTWARE_WEBSITE_URL=https://logiciels.test4.opentalent.fr
+###
+
 ###> symfony/mercure-bundle ###
 # See https://symfony.com/doc/current/mercure.html#configuration
 # The URL of the Mercure hub, used by the app to publish updates (can be a local URL)

+ 12 - 4
env/.env.test5

@@ -3,20 +3,28 @@ APP_ENV=test
 APP_DEBUG=1
 ###< symfony/framework-bundle ###
 
+### Logiciel
+ADMIN_BASE_URL=https://admin.test5.opentalent.fr
+###
+
 ###> api v1 ###
-API_LEG_BASE_URL=https://api.test5.opentalent.fr/api
-PUBLIC_API_LEG_BASE_URL=https://api.test5.opentalent.fr/api
+API_LEG_BASE_URL=https://api.test5.opentalent.fr
+PUBLIC_API_LEG_BASE_URL=https://api.test5.opentalent.fr
 ###< api v1 ###
 
 ###> api v2 ###
-API_BASE_URL=https://ap2i.test5.opentalent.fr/api
-PUBLIC_API_BASE_URL=https://ap2i.test5.opentalent.fr/api
+API_BASE_URL=https://ap2i.test5.opentalent.fr
+PUBLIC_API_BASE_URL=https://ap2i.test5.opentalent.fr
 ###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=https://sub.test5.opentalent.fr
 ###< typo3 client ###
 
+### Site logiciels
+SOFTWARE_WEBSITE_URL=https://logiciels.test5.opentalent.fr
+###
+
 ###> symfony/mercure-bundle ###
 # See https://symfony.com/doc/current/mercure.html#configuration
 # The URL of the Mercure hub, used by the app to publish updates (can be a local URL)

+ 12 - 4
env/.env.test6

@@ -3,20 +3,28 @@ APP_ENV=test
 APP_DEBUG=1
 ###< symfony/framework-bundle ###
 
+### Logiciel
+ADMIN_BASE_URL=https://admin.test6.opentalent.fr
+###
+
 ###> api v1 ###
-API_LEG_BASE_URL=https://api.test6.opentalent.fr/api
-PUBLIC_API_LEG_BASE_URL=https://api.test6.opentalent.fr/api
+API_LEG_BASE_URL=https://api.test6.opentalent.fr
+PUBLIC_API_LEG_BASE_URL=https://api.test6.opentalent.fr
 ###< api v1 ###
 
 ###> api v2 ###
-API_BASE_URL=https://ap2i.test6.opentalent.fr/api
-PUBLIC_API_BASE_URL=https://ap2i.test6.opentalent.fr/api
+API_BASE_URL=https://ap2i.test6.opentalent.fr
+PUBLIC_API_BASE_URL=https://ap2i.test6.opentalent.fr
 ###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=https://sub.test6.opentalent.fr
 ###< typo3 client ###
 
+### Site logiciels
+SOFTWARE_WEBSITE_URL=https://logiciels.test6.opentalent.fr
+###
+
 ###> symfony/mercure-bundle ###
 # See https://symfony.com/doc/current/mercure.html#configuration
 # The URL of the Mercure hub, used by the app to publish updates (can be a local URL)

+ 12 - 4
env/.env.test7

@@ -3,20 +3,28 @@ APP_ENV=test
 APP_DEBUG=1
 ###< symfony/framework-bundle ###
 
+### Logiciel
+ADMIN_BASE_URL=https://admin.test7.opentalent.fr
+###
+
 ###> api v1 ###
-API_LEG_BASE_URL=https://api.test7.opentalent.fr/api
-PUBLIC_API_LEG_BASE_URL=https://api.test7.opentalent.fr/api
+API_LEG_BASE_URL=https://api.test7.opentalent.fr
+PUBLIC_API_LEG_BASE_URL=https://api.test7.opentalent.fr
 ###< api v1 ###
 
 ###> api v2 ###
-API_BASE_URL=https://ap2i.test7.opentalent.fr/api
-PUBLIC_API_BASE_URL=https://ap2i.test7.opentalent.fr/api
+API_BASE_URL=https://ap2i.test7.opentalent.fr
+PUBLIC_API_BASE_URL=https://ap2i.test7.opentalent.fr
 ###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=https://sub.test7.opentalent.fr
 ###< typo3 client ###
 
+### Site logiciels
+SOFTWARE_WEBSITE_URL=https://logiciels.test7.opentalent.fr
+###
+
 ###> symfony/mercure-bundle ###
 # See https://symfony.com/doc/current/mercure.html#configuration
 # The URL of the Mercure hub, used by the app to publish updates (can be a local URL)

+ 12 - 4
env/.env.test8

@@ -3,20 +3,28 @@ APP_ENV=test
 APP_DEBUG=1
 ###< symfony/framework-bundle ###
 
+### Logiciel
+ADMIN_BASE_URL=https://admin.test8.opentalent.fr
+###
+
 ###> api v1 ###
-API_LEG_BASE_URL=https://api.test8.opentalent.fr/api
-PUBLIC_API_LEG_BASE_URL=https://api.test8.opentalent.fr/api
+API_LEG_BASE_URL=https://api.test8.opentalent.fr
+PUBLIC_API_LEG_BASE_URL=https://api.test8.opentalent.fr
 ###< api v1 ###
 
 ###> api v2 ###
-API_BASE_URL=https://ap2i.test8.opentalent.fr/api
-PUBLIC_API_BASE_URL=https://ap2i.test8.opentalent.fr/api
+API_BASE_URL=https://ap2i.test8.opentalent.fr
+PUBLIC_API_BASE_URL=https://ap2i.test8.opentalent.fr
 ###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=https://sub.test8.opentalent.fr
 ###< typo3 client ###
 
+### Site logiciels
+SOFTWARE_WEBSITE_URL=https://logiciels.test8.opentalent.fr
+###
+
 ###> symfony/mercure-bundle ###
 # See https://symfony.com/doc/current/mercure.html#configuration
 # The URL of the Mercure hub, used by the app to publish updates (can be a local URL)

+ 12 - 4
env/.env.test9

@@ -3,20 +3,28 @@ APP_ENV=test
 APP_DEBUG=1
 ###< symfony/framework-bundle ###
 
+### Logiciel
+ADMIN_BASE_URL=https://admin.test9.opentalent.fr
+###
+
 ###> api v1 ###
-API_LEG_BASE_URL=https://api.test9.opentalent.fr/api
-PUBLIC_API_LEG_BASE_URL=https://api.test9.opentalent.fr/api
+API_LEG_BASE_URL=https://api.test9.opentalent.fr
+PUBLIC_API_LEG_BASE_URL=https://api.test9.opentalent.fr
 ###< api v1 ###
 
 ###> api v2 ###
-API_BASE_URL=https://ap2i.test9.opentalent.fr/api
-PUBLIC_API_BASE_URL=https://ap2i.test9.opentalent.fr/api
+API_BASE_URL=https://ap2i.test9.opentalent.fr
+PUBLIC_API_BASE_URL=https://ap2i.test9.opentalent.fr
 ###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=https://sub.test9.opentalent.fr
 ###< typo3 client ###
 
+### Site logiciels
+SOFTWARE_WEBSITE_URL=https://logiciels.test9.opentalent.fr
+###
+
 ###> symfony/mercure-bundle ###
 # See https://symfony.com/doc/current/mercure.html#configuration
 # The URL of the Mercure hub, used by the app to publish updates (can be a local URL)

BIN
public/images/facebook.jpg


BIN
public/images/linkedin.jpg


BIN
public/images/youtube.jpg


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

+ 5 - 0
src/ApiResources/Core/File/Image.php

@@ -23,6 +23,11 @@ use Symfony\Component\Validator\Constraints as Assert;
             security: 'is_granted("ROLE_FILE")',
             provider: ImageProvider::class
         ),
+        new Get(
+            uriTemplate: '/internal/image/download/{fileId}/{size}',
+            requirements: ['fileId' => '\\d+', 'size' => new EnumRequirement(FileSizeEnum::class)],
+            provider: ImageProvider::class
+        ),
     ]
 )]
 class Image

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

@@ -0,0 +1,164 @@
+<?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\Booking\EventGender;
+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 App\Validator\Constraints as OpentalentAssert;
+use Doctrine\Common\Collections\ArrayCollection;
+use JetBrains\PhpStorm\Pure;
+use Symfony\Component\ObjectMapper\Attribute\Map;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * 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\FieldLesserThan(field: 'datetimeStart', comparedTo: 'datetimeEnd')]
+#[OpentalentAssert\FieldLesserThan(field: 'priceMini', comparedTo: 'priceMaxi')]
+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;
+
+    #[Assert\Url(protocols: ['http', 'https'])]
+    public ?string $url = null;
+
+    #[Assert\Url(protocols: ['http', 'https'])]
+    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 ?EventGender $gender;
+
+    #[Assert\Positive()]
+    public ?float $priceMini = null;
+    #[Assert\Positive()]
+    public ?float $priceMaxi = null;
+
+    #[Pure]
+    public function __construct()
+    {
+        $this->categories = new ArrayCollection();
+    }
+
+    /**
+     * @return array<Categories>
+     */
+    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;
+    }
+}

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

@@ -0,0 +1,136 @@
+<?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\Entity\Organization\TypeOfPractice;
+use App\Enum\Organization\LegalEnum;
+use App\Enum\Organization\PrincipalTypeEnum;
+use App\State\Processor\Freemium\FreemiumOrganizationProcessor;
+use App\State\Provider\Freemium\FreemiumOrganizationProvider;
+use Doctrine\Common\Collections\ArrayCollection;
+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;
+
+    public ?string $description = null;
+
+    public ?LegalEnum $legalStatus = null;
+
+    public ?PrincipalTypeEnum $principalType = null;
+
+    #[Map(if: false)]
+    public ArrayCollection $typeOfPractices;
+
+    #[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)]
+    #[Assert\Url(protocols: ['http', 'https'])]
+    public ?string $facebook = null;
+
+    #[Assert\Length(max: 255)]
+    #[Assert\Url(protocols: ['http', 'https'])]
+    public ?string $twitter = null;
+
+    #[Assert\Length(max: 255)]
+    #[Assert\Url(protocols: ['http', 'https'])]
+    public ?string $youtube = null;
+
+    #[Assert\Length(max: 255)]
+    #[Assert\Url(protocols: ['http', 'https'])]
+    public ?string $instagram = null;
+
+    public bool $portailVisibility = true;
+
+    public ?File $logo = null;
+
+    /**
+     * @return array<TypeOfPractice>
+     */
+    public function getTypeOfPractices(): array
+    {
+        // retourne un tableau proprement indexé
+        return array_values($this->typeOfPractices->toArray());
+    }
+
+    public function addTypeOfPractice(TypeOfPractice $typeOfPractices): self
+    {
+        if (!$this->typeOfPractices->contains($typeOfPractices)) {
+            $this->typeOfPractices[] = $typeOfPractices;
+        }
+
+        return $this;
+    }
+
+    public function removeTypeOfPractice(TypeOfPractice $typeOfPractices): self
+    {
+        $this->typeOfPractices->removeElement($typeOfPractices);
+
+        return $this;
+    }
+}

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

+ 5 - 8
src/ApiResources/Organization/OrganizationCreationRequest.php

@@ -12,6 +12,7 @@ use App\Enum\Organization\OrganizationIdsEnum;
 use App\Enum\Organization\PrincipalTypeEnum;
 use App\Enum\Organization\SettingsProductEnum;
 use App\State\Processor\Organization\OrganizationCreationRequestProcessor;
+use libphonenumber\PhoneNumber;
 use Symfony\Component\Validator\Constraints as Assert;
 
 /**
@@ -88,11 +89,7 @@ class OrganizationCreationRequest
 
     private int $countryId = self::FRANCE_COUNTRY_INTERNAL_ID;
 
-    #[Assert\Length(
-        min: 10,
-        minMessage: 'Phone number must be at least {{ limit }} characters long',
-    )]
-    private string $phoneNumber;
+    private PhoneNumber $phoneNumber;
 
     #[Assert\Email(
         message: 'The email {{ value }} is not a valid email.',
@@ -106,7 +103,7 @@ class OrganizationCreationRequest
     private string $subdomain;
 
     #[Assert\Positive]
-    private int $parentId = OrganizationIdsEnum::_2IOS->value;
+    private int $parentId = OrganizationIdsEnum::OPENTALENT->value;
 
     private PrincipalTypeEnum $principalType;
 
@@ -313,12 +310,12 @@ class OrganizationCreationRequest
         return $this;
     }
 
-    public function getPhoneNumber(): string
+    public function getPhoneNumber(): PhoneNumber
     {
         return $this->phoneNumber;
     }
 
-    public function setPhoneNumber(string $phoneNumber): self
+    public function setPhoneNumber(PhoneNumber $phoneNumber): self
     {
         $this->phoneNumber = $phoneNumber;
 

+ 7 - 14
src/ApiResources/Organization/OrganizationMemberCreationRequest.php

@@ -6,6 +6,7 @@ namespace App\ApiResources\Organization;
 
 use App\Enum\Core\FileTypeEnum;
 use App\Enum\Person\GenderEnum;
+use libphonenumber\PhoneNumber;
 use Symfony\Component\Validator\Constraints as Assert;
 
 /**
@@ -55,17 +56,9 @@ class OrganizationMemberCreationRequest
 
     private int $countryId = 72; // France's id
 
-    #[Assert\Length(
-        min: 10,
-        minMessage: 'Phone number must be at least {{ limit }} characters long',
-    )]
-    private ?string $phone = null;
+    private ?PhoneNumber $phone = null;
 
-    #[Assert\Length(
-        min: 10,
-        minMessage: 'Mobile phone number must be at least {{ limit }} characters long',
-    )]
-    private ?string $mobile = null;
+    private ?PhoneNumber $mobile = null;
 
     #[Assert\Email(
         message: 'The email {{ value }} is not a valid email.',
@@ -192,24 +185,24 @@ class OrganizationMemberCreationRequest
         return $this;
     }
 
-    public function getPhone(): ?string
+    public function getPhone(): ?PhoneNumber
     {
         return $this->phone;
     }
 
-    public function setPhone(?string $phone): self
+    public function setPhone(?PhoneNumber $phone): self
     {
         $this->phone = $phone;
 
         return $this;
     }
 
-    public function getMobile(): ?string
+    public function getMobile(): ?PhoneNumber
     {
         return $this->mobile;
     }
 
-    public function setMobile(?string $mobile): self
+    public function setMobile(?PhoneNumber $mobile): self
     {
         $this->mobile = $mobile;
 

+ 5 - 0
src/ApiResources/Organization/Subdomain/SubdomainAvailability.php

@@ -11,11 +11,16 @@ use App\State\Provider\Organization\Subdomain\SubdomainAvailabilityProvider;
 
 #[ApiResource(
     operations: [
+        // TODO: route à supprimer quand elle ne sera plus utilisée nulle part, on ne conservera que la route publique
         new Get(
             uriTemplate: '/subdomains/is_available',
             security: 'is_granted("ROLE_ORGANIZATION_VIEW") or is_granted("ROLE_ORGANIZATION")',
             provider: SubdomainAvailabilityProvider::class
         ),
+        new Get(
+            uriTemplate: '/public/subdomains/is_available',
+            provider: SubdomainAvailabilityProvider::class
+        ),
     ]
 )]
 class SubdomainAvailability

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

+ 354 - 0
src/ApiResources/Shop/NewStructureArtistPremiumTrialRequest.php

@@ -0,0 +1,354 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\ApiResources\Shop;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Post;
+use App\Enum\Organization\LegalEnum;
+use App\Enum\Organization\PrincipalTypeEnum;
+use App\State\Processor\Shop\NewStructureArtistPremiumTrialRequestProcessor;
+use libphonenumber\PhoneNumber;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * New structure trial request for the artist premium product.
+ *
+ * This API resource represents a request for a trial of the Artist Premium product for a new structure.
+ * It contains all the information needed to create a new organization and start a premium trial:
+ * - Structure information (name, address, email, type, legal status, etc.)
+ * - Representative information (name, function, email, phone, etc.)
+ * - Acceptance of terms and conditions
+ *
+ * The resource is exposed through API Platform for submission via a POST endpoint.
+ */
+#[ApiResource(
+    operations: [
+        new Post(
+            uriTemplate: '/public/shop/new-structure-artist-premium-trial-request',
+            processor: NewStructureArtistPremiumTrialRequestProcessor::class
+        ),
+    ]
+)]
+class NewStructureArtistPremiumTrialRequest implements ShopRequestData
+{
+    /**
+     * Id 'bidon' ajouté par défaut pour permettre la construction
+     * de l'IRI par api platform.
+     */
+    #[ApiProperty(identifier: true)]
+    private int $id = 0;
+
+    #[Assert\Length(
+        min: 2,
+        minMessage: "Structure's name must be at least {{ limit }} characters long",
+    )]
+    private string $structureName;
+
+    #[Assert\NotBlank]
+    private string $address;
+
+    private ?string $addressComplement = null;
+
+    #[Assert\Length(
+        min: 3,
+        minMessage: 'Postal code must be at least {{ limit }} characters long',
+    )]
+    private string $postalCode;
+
+    #[Assert\NotBlank]
+    private string $city;
+
+    #[Assert\NotBlank]
+    #[Assert\Email(message: 'The email {{ value }} is not a valid email.')]
+    private string $structureEmail;
+
+    private PrincipalTypeEnum $structureType;
+
+    private LegalEnum $legalStatus;
+
+    #[Assert\NotBlank]
+    #[Assert\Regex(pattern: '/^[a-z0-9][a-z0-9-]{0,28}[a-z0-9]$/')]
+    private string $structureIdentifier;
+
+    #[Assert\Regex(pattern: '/^|(\d{9})$/')]
+    private string $siren;
+
+    #[Assert\NotBlank]
+    private string $representativeFirstName;
+
+    #[Assert\NotBlank]
+    private string $representativeLastName;
+
+    #[Assert\NotBlank]
+    private string $representativeFunction;
+
+    #[Assert\NotBlank]
+    #[Assert\Email(message: 'The email {{ value }} is not a valid email.')]
+    private string $representativeEmail;
+
+    #[Assert\NotBlank]
+    private ?PhoneNumber $representativePhone;
+
+    #[Assert\IsTrue(message: 'terms-must-be-accepted')]
+    private bool $termsAccepted = false;
+
+    private bool $legalRepresentative = false;
+
+    private bool $newsletterSubscription = false;
+
+    /**
+     * Password of the admin account of the newly created structure.
+     *
+     * Must have at least 8 characters, including at least one uppercase letter,
+     * one lowercase letter, one digit, and one special character.
+     */
+    #[Assert\NotBlank]
+    #[Assert\Regex(
+        pattern: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\da-zA-Z]).{8,}$/',
+        message: 'Password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one digit, and one special character.'
+    )]
+    private string $password;
+
+    public function getId(): int
+    {
+        return $this->id;
+    }
+
+    public function setId(int $id): self
+    {
+        $this->id = $id;
+
+        return $this;
+    }
+
+    public function getStructureName(): string
+    {
+        return $this->structureName;
+    }
+
+    public function setStructureName(string $structureName): self
+    {
+        $this->structureName = $structureName;
+
+        return $this;
+    }
+
+    public function getAddress(): string
+    {
+        return $this->address;
+    }
+
+    public function setAddress(string $address): self
+    {
+        $this->address = $address;
+
+        return $this;
+    }
+
+    public function getAddressComplement(): ?string
+    {
+        return $this->addressComplement;
+    }
+
+    public function setAddressComplement(?string $addressComplement): self
+    {
+        $this->addressComplement = $addressComplement;
+
+        return $this;
+    }
+
+    public function getPostalCode(): string
+    {
+        return $this->postalCode;
+    }
+
+    public function setPostalCode(string $postalCode): self
+    {
+        $this->postalCode = $postalCode;
+
+        return $this;
+    }
+
+    public function getCity(): string
+    {
+        return $this->city;
+    }
+
+    public function setCity(string $city): self
+    {
+        $this->city = $city;
+
+        return $this;
+    }
+
+    public function getStructureEmail(): string
+    {
+        return $this->structureEmail;
+    }
+
+    public function setStructureEmail(string $structureEmail): self
+    {
+        $this->structureEmail = $structureEmail;
+
+        return $this;
+    }
+
+    public function getStructureType(): PrincipalTypeEnum
+    {
+        return $this->structureType;
+    }
+
+    public function setStructureType(PrincipalTypeEnum $structureType): self
+    {
+        $this->structureType = $structureType;
+
+        return $this;
+    }
+
+    public function getLegalStatus(): LegalEnum
+    {
+        return $this->legalStatus;
+    }
+
+    public function setLegalStatus(LegalEnum $legalStatus): self
+    {
+        $this->legalStatus = $legalStatus;
+
+        return $this;
+    }
+
+    public function getStructureIdentifier(): string
+    {
+        return $this->structureIdentifier;
+    }
+
+    public function setStructureIdentifier(string $structureIdentifier): self
+    {
+        $this->structureIdentifier = $structureIdentifier;
+
+        return $this;
+    }
+
+    public function getSiren(): string
+    {
+        return $this->siren;
+    }
+
+    public function setSiren(string $siren): self
+    {
+        $this->siren = $siren;
+
+        return $this;
+    }
+
+    public function getRepresentativeFirstName(): string
+    {
+        return $this->representativeFirstName;
+    }
+
+    public function setRepresentativeFirstName(string $representativeFirstName): self
+    {
+        $this->representativeFirstName = $representativeFirstName;
+
+        return $this;
+    }
+
+    public function getRepresentativeLastName(): string
+    {
+        return $this->representativeLastName;
+    }
+
+    public function setRepresentativeLastName(string $representativeLastName): self
+    {
+        $this->representativeLastName = $representativeLastName;
+
+        return $this;
+    }
+
+    public function getRepresentativeFunction(): string
+    {
+        return $this->representativeFunction;
+    }
+
+    public function setRepresentativeFunction(string $representativeFunction): self
+    {
+        $this->representativeFunction = $representativeFunction;
+
+        return $this;
+    }
+
+    public function getRepresentativeEmail(): string
+    {
+        return $this->representativeEmail;
+    }
+
+    public function setRepresentativeEmail(string $representativeEmail): self
+    {
+        $this->representativeEmail = $representativeEmail;
+
+        return $this;
+    }
+
+    public function getRepresentativePhone(): ?PhoneNumber
+    {
+        return $this->representativePhone;
+    }
+
+    public function setRepresentativePhone(?PhoneNumber $representativePhone): self
+    {
+        $this->representativePhone = $representativePhone;
+
+        return $this;
+    }
+
+    public function getTermsAccepted(): bool
+    {
+        return $this->termsAccepted;
+    }
+
+    public function setTermsAccepted(bool $termsAccepted): self
+    {
+        $this->termsAccepted = $termsAccepted;
+
+        return $this;
+    }
+
+    public function getLegalRepresentative(): bool
+    {
+        return $this->legalRepresentative;
+    }
+
+    public function setLegalRepresentative(bool $legalRepresentative): self
+    {
+        $this->legalRepresentative = $legalRepresentative;
+
+        return $this;
+    }
+
+    public function getNewsletterSubscription(): bool
+    {
+        return $this->newsletterSubscription;
+    }
+
+    public function setNewsletterSubscription(bool $newsletterSubscription): self
+    {
+        $this->newsletterSubscription = $newsletterSubscription;
+
+        return $this;
+    }
+
+    public function getPassword(): string
+    {
+        return $this->password;
+    }
+
+    public function setPassword(string $password): self
+    {
+        $this->password = $password;
+
+        return $this;
+    }
+}

+ 9 - 0
src/ApiResources/Shop/ShopRequestData.php

@@ -0,0 +1,9 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\ApiResources\Shop;
+
+interface ShopRequestData
+{
+}

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

+ 2 - 4
src/Doctrine/Access/AdditionalExtension/DateTimeConstraintExtensionAdditional.php

@@ -20,11 +20,9 @@ class DateTimeConstraintExtensionAdditional implements AdditionalAccessExtension
 
     public function support(string $name): bool
     {
-        return
+        $request = $this->requestStack->getMainRequest();
 
-                $this->requestStack->getMainRequest()->isMethod('GET')
-                && $this->requestStack->getMainRequest()->get('_time_constraint', true) == true
-        ;
+        return $request->isMethod('GET') && $request->get('_time_constraint', true);
     }
 
     public function addWhere(QueryBuilder $queryBuilder): void

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

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

@@ -12,6 +12,8 @@ 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 App\Enum\Booking\VisibilityEnum;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
@@ -88,6 +90,37 @@ 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(nullable: true)]
+    #[Assert\Url(
+        message: 'url-error',
+        protocols: ['http', 'https']
+    )]
+    protected ?string $url = null;
+
+    #[ORM\Column(nullable: true)]
+    #[Assert\Url(
+        message: 'url-ticket-error',
+        protocols: ['http', 'https']
+    )]
+    protected ?string $urlTicket = null;
+
+    #[ORM\Column(length: 255, nullable: true, enumType: PricingEventEnum::class)]
+    private ?PricingEventEnum $pricing = null;
+
+    #[ORM\Column(nullable: true)]
+    #[Assert\Positive]
+    protected ?float $priceMini = null;
+
+    #[ORM\Column(nullable: true)]
+    #[Assert\Positive]
+    protected ?float $priceMaxi = null;
+
+    #[ORM\Column(length: 255, nullable: false, enumType: VisibilityEnum::class)]
+    protected VisibilityEnum $visibility;
+
     public function __construct()
     {
         $this->eventRecur = new ArrayCollection();
@@ -288,6 +321,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 +424,88 @@ 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;
+    }
+
+    public function getVisibility(): ?VisibilityEnum
+    {
+        return $this->visibility;
+    }
+
+    public function setVisibility(?VisibilityEnum $visibility): self
+    {
+        $this->visibility = $visibility;
+
+        return $this;
+    }
 }

+ 31 - 1
src/Entity/Booking/EventGender.php

@@ -5,14 +5,28 @@ declare(strict_types=1);
 namespace App\Entity\Booking;
 
 // use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Auditable;
+use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
+use ApiPlatform\Metadata\ApiFilter;
 use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
+use App\Enum\Booking\EventGenderTypeEnum;
 use Doctrine\ORM\Mapping as ORM;
 
 /**
  * Enum des genres d'évènements.
  */
 // #[Auditable]
-#[ApiResource(operations: [])]
+#[ApiResource(
+    operations: [
+        new Get(security: 'is_granted(\'ROLE_EVENTS\') or is_granted(\'ROLE_USER_FREEMIUM\')'),
+        new GetCollection(
+            paginationEnabled: false,
+            security: 'is_granted(\'ROLE_EVENTS\') or is_granted(\'ROLE_USER_FREEMIUM\')'
+        ),
+    ]
+)]
+#[ApiFilter(filterClass: SearchFilter::class, properties: ['type' => 'exact'])]
 #[ORM\Entity]
 class EventGender
 {
@@ -21,8 +35,24 @@ class EventGender
     #[ORM\GeneratedValue]
     private ?int $id = null;
 
+    #[ORM\Column]
+    protected string $name;
+
+    #[ORM\Column(length: 255, nullable: false, enumType: EventGenderTypeEnum::class)]
+    protected EventGenderTypeEnum $type;
+
     public function getId(): ?int
     {
         return $this->id;
     }
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    public function getType(): EventGenderTypeEnum
+    {
+        return $this->type;
+    }
 }

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

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

@@ -21,8 +21,23 @@ 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;
+    }
+}

+ 13 - 8
src/Entity/Organization/TypeOfPractice.php

@@ -5,6 +5,8 @@ declare(strict_types=1);
 namespace App\Entity\Organization;
 
 use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
 use App\Enum\Cotisation\CategoryTypeOfPracticeEnum;
 use App\Enum\Cotisation\TypeOfPracticeEnum;
 use App\Repository\Organization\TypeOfPracticeRepository;
@@ -13,12 +15,20 @@ use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
 use JetBrains\PhpStorm\Pure;
-use Symfony\Component\Serializer\Annotation\Groups;
 
 /**
  * Type des pratique d'une organisation.
  */
-#[ApiResource(operations: [])]
+#[ApiResource(
+    operations: [
+        new Get(
+            security: 'is_granted(\'ROLE_GENERAL_CONFIG\') or is_granted(\'ROLE_USER_FREEMIUM\')'),
+        new GetCollection(
+            paginationEnabled: false,
+            security: 'is_granted(\'ROLE_GENERAL_CONFIG\') or is_granted(\'ROLE_USER_FREEMIUM\')'
+        ),
+    ]
+)]
 // #[Auditable]
 #[ORM\Entity(repositoryClass: TypeOfPracticeRepository::class)]
 class TypeOfPractice
@@ -26,22 +36,17 @@ class TypeOfPractice
     #[ORM\Id]
     #[ORM\Column]
     #[ORM\GeneratedValue]
-    #[Groups(['read'])]
     private ?int $id = null;
 
     #[ORM\Column(length: 50, nullable: true, enumType: TypeOfPracticeEnum::class)]
-    #[Groups(['read'])]
     private ?TypeOfPracticeEnum $name = null;
 
     #[ORM\Column(length: 50, nullable: true, enumType: CategoryTypeOfPracticeEnum::class)]
-    #[Groups(['read'])]
     private ?CategoryTypeOfPracticeEnum $category = null;
 
     /** @var Collection<int, Organization> */
-    #[ORM\ManyToMany(targetEntity: Organization::class, inversedBy: 'typeOfPractices')]
+    #[ORM\ManyToMany(targetEntity: Organization::class, inversedBy: 'typeOfPractices', fetch: 'EXTRA_LAZY')]
     #[ORM\JoinTable(name: 'organization_type_of_practices')]
-    #[ORM\JoinColumn(name: 'typeofpractice_id', referencedColumnName: 'id')]
-    #[ORM\InverseJoinColumn(name: 'organization_id', referencedColumnName: 'id')]
     private Collection $organizations;
 
     #[Pure]

+ 16 - 1
src/Entity/Place/AbstractPlace.php

@@ -33,9 +33,12 @@ 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;
+    private ?AddressPostal $addressPostal = null;
 
     /** @var Collection<int, Tagg> */
     #[ORM\ManyToMany(targetEntity: Tagg::class, inversedBy: 'places', cascade: ['persist'])]
@@ -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;

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

@@ -13,6 +13,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 +23,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> */

+ 119 - 0
src/Entity/Shop/ShopRequest.php

@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Entity\Shop;
+
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use App\Enum\Shop\ShopRequestStatus;
+use App\Enum\Shop\ShopRequestType;
+use App\State\Provider\Shop\ShopRequestProvider;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * Une demande effectuée par un client via la boutique en ligne (ex: demande d'essai premium).
+ */
+#[ApiResource(operations: [
+    new Get(
+        uriTemplate: '/public/shop/validate/{token}',
+        provider: ShopRequestProvider::class,
+    ),
+])]
+#[ORM\Entity]
+class ShopRequest
+{
+    #[ORM\Id]
+    #[ORM\Column(type: 'guid')]
+    private string $token;
+
+    #[ORM\Column(type: 'datetime_immutable')]
+    private \DateTimeImmutable $submissionDate;
+
+    #[ORM\Column(length: 50, enumType: ShopRequestStatus::class)]
+    private ShopRequestStatus $status;
+
+    #[ORM\Column(length: 50, enumType: ShopRequestType::class)]
+    private ShopRequestType $type;
+
+    /**
+     * @var array<string, mixed>
+     */
+    #[ORM\Column(type: Types::JSON)]
+    private array $data = [];
+
+    public function __construct()
+    {
+        $this->submissionDate = new \DateTimeImmutable();
+        $this->status = ShopRequestStatus::PENDING;
+    }
+
+    public function getToken(): string
+    {
+        return $this->token;
+    }
+
+    public function setToken(string $token): self
+    {
+        $this->token = $token;
+
+        return $this;
+    }
+
+    public function getSubmissionDate(): \DateTimeImmutable
+    {
+        return $this->submissionDate;
+    }
+
+    public function setSubmissionDate(\DateTimeImmutable $submissionDate): self
+    {
+        $this->submissionDate = $submissionDate;
+
+        return $this;
+    }
+
+    public function getStatus(): ?ShopRequestStatus
+    {
+        return $this->status;
+    }
+
+    public function setStatus(?ShopRequestStatus $status): self
+    {
+        $this->status = $status;
+
+        return $this;
+    }
+
+    public function getType(): ShopRequestType
+    {
+        return $this->type;
+    }
+
+    public function setType(ShopRequestType $type): self
+    {
+        $this->type = $type;
+
+        return $this;
+    }
+
+    /**
+     * @return array<string, mixed>
+     */
+    public function getData(): array
+    {
+        return $this->data;
+    }
+
+    /**
+     * @param array<string, mixed> $data
+     *
+     * @return $this
+     */
+    public function setData(array $data): self
+    {
+        $this->data = $data;
+
+        return $this;
+    }
+}

+ 22 - 0
src/Enum/Access/AccessIdsEnum.php

@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Enum\Access;
+
+use App\Enum\EnumMethodsTrait;
+
+/**
+ * Id de comptes spécifiques.
+ *
+ * This enum defines specific account IDs used in the application:
+ * - ADMIN_2IOPENSERVICE: ID of the admin2iopenservice account (10984)
+ *
+ * These IDs are used for system operations like sending emails from specific accounts.
+ */
+enum AccessIdsEnum: int
+{
+    use EnumMethodsTrait;
+
+    case ADMIN_2IOPENSERVICE = 10984;
+}

+ 19 - 0
src/Enum/Booking/EventGenderTypeEnum.php

@@ -0,0 +1,19 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Enum\Booking;
+
+use App\Enum\EnumMethodsTrait;
+
+/**
+ * Groupe des genre des événements.
+ */
+enum EventGenderTypeEnum: string
+{
+    use EnumMethodsTrait;
+
+    case PEDAGOGIC_EVENT = 'PEDAGOGIC_EVENT';
+    case CULTURAL_EVENT = 'CULTURAL_EVENT';
+    case INTERNAL_EVENT = 'INTERNAL_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';
+}

+ 2 - 0
src/Enum/Cotisation/CategoryTypeOfPracticeEnum.php

@@ -18,4 +18,6 @@ enum CategoryTypeOfPracticeEnum: string
     case CATEGORY_CHORUS = 'CATEGORY_CHORUS';
     case CATEGORY_BAND = 'CATEGORY_BAND';
     case CATEGORY_OTHER = 'CATEGORY_OTHER';
+    case CATEGORY_DANCES = 'CATEGORY_DANCES';
+    case CATEGORY_TEACHING = 'CATEGORY_TEACHING';
 }

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

@@ -17,5 +17,5 @@ enum OrganizationIdsEnum: int
     case _2IOS = 32366;
     case FFEC = 91295;
     case OPENTALENT_BASE = 13;
-    case OUTOFNET_PARENT = 93931;
+    case OPENTALENT = 93931;
 }

+ 19 - 0
src/Enum/Organization/PrincipalTypeShortListEnum.php

@@ -0,0 +1,19 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Enum\Organization;
+
+use App\Enum\EnumMethodsTrait;
+
+/**
+ * Types principaux raccourci d'une organisation.
+ */
+enum PrincipalTypeShortListEnum: string
+{
+    use EnumMethodsTrait;
+
+    case ARTISTIC_EDUCATION_ONLY = 'ARTISTIC_EDUCATION_ONLY';
+    case ARTISTIC_PRACTICE_ONLY = 'ARTISTIC_PRACTICE_ONLY';
+    case ARTISTIC_PRACTICE_EDUCATION = 'ARTISTIC_PRACTICE_EDUCATION';
+}

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

+ 28 - 0
src/Enum/Shop/ShopRequestStatus.php

@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Enum\Shop;
+
+use App\Enum\EnumMethodsTrait;
+
+/**
+ * Statuts d'une ShopRequest.
+ *
+ * This enum defines the possible statuses for a shop request:
+ * - PENDING: Initial status when the request is created
+ * - ACTIVATION_LINK_SENT: Status when the activation link has been sent to the user
+ * - VALIDATED: Status when the user has validated the request by clicking the activation link
+ * - COMPLETED: Status when the request has been fully processed
+ * - ERROR: Status when an error occurred during processing
+ */
+enum ShopRequestStatus: string
+{
+    use EnumMethodsTrait;
+
+    case PENDING = 'PENDING';
+    case ACTIVATION_LINK_SENT = 'ACTIVATION_LINK_SENT';
+    case VALIDATED = 'VALIDATED';
+    case COMPLETED = 'COMPLETED';
+    case ERROR = 'ERROR';
+}

+ 22 - 0
src/Enum/Shop/ShopRequestType.php

@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Enum\Shop;
+
+use App\Enum\EnumMethodsTrait;
+
+/**
+ * Type of shop request.
+ *
+ * This enum defines the possible types of shop requests:
+ * - NEW_STRUCTURE_ARTIST_PREMIUM_TRIAL: Request for a trial of the Artist Premium product for a new structure
+ *
+ * Additional types can be added as new features are implemented.
+ */
+enum ShopRequestType: string
+{
+    use EnumMethodsTrait;
+
+    case NEW_STRUCTURE_ARTIST_PREMIUM_TRIAL = 'NEW_STRUCTURE_ARTIST_PREMIUM_TRIAL';
+}

+ 2 - 10
src/EventListener/OnKernelRequestPreRead.php

@@ -10,14 +10,12 @@ use App\Service\Doctrine\FiltersConfigurationService;
 use App\Service\Utils\ObjectUtils;
 use Symfony\Bundle\SecurityBundle\Security;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\HttpKernel\Event\RequestEvent;
 use Symfony\Component\HttpKernel\KernelEvents;
 
 class OnKernelRequestPreRead implements EventSubscriberInterface
 {
     public function __construct(
-        private RequestStack $requestStack,
         private Security $security,
         private FiltersConfigurationService $filtersConfigurationService,
     ) {
@@ -37,17 +35,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`)',

+ 36 - 0
src/Message/Handler/Shop/NewStructureArtistPremiumTrialHandler.php

@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Message\Handler\Shop;
+
+use App\Message\Message\Shop\NewStructureArtistPremiumTrial;
+use App\Service\Shop\ShopService;
+use Symfony\Component\Messenger\Attribute\AsMessageHandler;
+
+/**
+ * Message handler for processing new structure artist premium trial requests.
+ *
+ * This class handles the NewStructureArtistPremiumTrial message, which is dispatched
+ * when a user validates a trial request by clicking the activation link in the email.
+ *
+ * It delegates the actual processing to the ShopService.
+ */
+#[AsMessageHandler]
+readonly class NewStructureArtistPremiumTrialHandler
+{
+    public function __construct(
+        private ShopService $shopService,
+    ) {
+    }
+
+    /**
+     * @param NewStructureArtistPremiumTrial $message The message to process
+     */
+    public function __invoke(NewStructureArtistPremiumTrial $message): void
+    {
+        $token = $message->getToken();
+
+        $this->shopService->handleNewStructureArtistPremiumTrialRequest($token);
+    }
+}

+ 42 - 0
src/Message/Message/Shop/NewStructureArtistPremiumTrial.php

@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Message\Message\Shop;
+
+/**
+ * Message for processing a new structure artist premium trial request.
+ *
+ * This message is dispatched when a user validates a trial request by clicking
+ * the activation link in the email. It contains the token that identifies the
+ * shop request to be processed.
+ *
+ * It is handled by the NewStructureArtistPremiumTrialHandler class.
+ */
+class NewStructureArtistPremiumTrial
+{
+    public function __construct(
+        private string $token,
+    ) {
+    }
+
+    /**
+     * Gets the token that identifies the shop request.
+     *
+     * @return string The token
+     */
+    public function getToken(): string
+    {
+        return $this->token;
+    }
+
+    /**
+     * Sets the token that identifies the shop request.
+     *
+     * @param string $token The token
+     */
+    public function setToken(string $token): void
+    {
+        $this->token = $token;
+    }
+}

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

@@ -73,7 +73,7 @@ class AccessRepository extends ServiceEntityRepository implements UserLoaderInte
     /**
      * Retourne l'access administrateur de cette organisation.
      */
-    public function findAdminAccess(Organization $organization): Access
+    public function findAdminAccess(Organization $organization): ?Access
     {
         return $this->findOneBy([
             'adminAccess' => 1,

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

@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Repository\Place;
+
+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);
     }
 
     /**

+ 9 - 7
src/Security/Voter/ModuleVoter.php

@@ -43,10 +43,10 @@ class ModuleVoter extends Voter
         }
 
         $resourceMetadata = $this->resourceMetadataFactory->create($subject->attributes->get('_api_resource_class'));
-        $module = $this->module->getModuleByResourceName($resourceMetadata->getOperation()->getShortName());
+        $modules = $this->module->getModulesByResourceName($resourceMetadata->getOperation()->getShortName());
 
         // Check if there is a module for this entity : eq configuration problem
-        if ($module === null) {
+        if (empty($modules)) {
             throw new AccessDeniedHttpException(sprintf('There are no module for the entity (%s) !', $resourceMetadata->getOperation()->getShortName()));
         }
 
@@ -55,8 +55,8 @@ class ModuleVoter extends Voter
         /** @var Organization $organization */
         $organization = $currentAccess->getOrganization();
 
-        if (!$this->isOrganizationHaveThisModule($organization, $module)) {
-            throw new AccessDeniedHttpException(sprintf("The organization doesn't have the module '%s'", $module));
+        if (!$this->isOrganizationHaveThisModule($organization, $modules)) {
+            throw new AccessDeniedHttpException(sprintf("The organization doesn't have one of the module"));
         }
 
         return true;
@@ -64,12 +64,14 @@ class ModuleVoter extends Voter
 
     /**
      * Test si l'organisation possède le module parmis les modules possédés via le produit souscrit, les options souscrites
-     * ou les modules possédées via des conditions particulières (isCmf par exemple).
+     *  ou les modules possédées via des conditions particulières (isCmf par exemple).
+     *
+     * @param array<string> $modules
      */
-    private function isOrganizationHaveThisModule(Organization $organization, string $module): bool
+    private function isOrganizationHaveThisModule(Organization $organization, array $modules): bool
     {
         $organizationModules = $this->module->getOrganizationModules($organization);
 
-        return in_array($module, $organizationModules);
+        return !empty(array_intersect($organizationModules, $modules));
     }
 }

+ 0 - 1
src/Service/ApiLegacy/ApiLegacyRequestService.php

@@ -45,7 +45,6 @@ class ApiLegacyRequestService extends ApiRequestService
         $headers = [
             'Accept' => '*/*',
             'Charset' => 'UTF-8',
-            'Accept-Encoding' => 'gzip, deflate, br',
             'Content-Type' => 'application/ld+json',
         ];
 

+ 138 - 0
src/Service/ApiResourceBuilder/Freemium/EventMappingBuilder.php

@@ -0,0 +1,138 @@
+<?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 App\Enum\Booking\VisibilityEnum;
+use Doctrine\ORM\EntityManagerInterface;
+use Ramsey\Uuid\Uuid;
+
+/**
+ * Mapping des informations d'un Event avec comme source un FreemiumEvent.
+ */
+class EventMappingBuilder
+{
+    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): void
+    {
+        $this->mapEventInformations($event, $freemiumEvent);
+        $this->mapEventPlaceInformations($event, $freemiumEvent);
+    }
+
+    /**
+     * Mapping des informations générales.
+     *
+     * @param Event         $event         : objet target
+     * @param FreemiumEvent $freemiumEvent : objet source
+     */
+    protected function mapEventInformations(Event $event, FreemiumEvent $freemiumEvent): void
+    {
+        // 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);
+        $event->setGender($freemiumEvent->gender);
+        $event->setVisibility(VisibilityEnum::PUBLIC_VISIBILITY);
+        $event->setUuid(Uuid::uuid4()->toString());
+
+        // Catégories
+        $event->removeAllCategories();
+        foreach ($freemiumEvent->categories as $category) {
+            $event->addCategory($category);
+        }
+    }
+
+    /**
+     * Recherche et mapping du lieu de lévénement.
+     */
+    protected function mapEventPlaceInformations(Event $event, FreemiumEvent $freemiumEvent): void
+    {
+        $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.
+     */
+    protected 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
+            ->setOrganization($freemiumEvent->organization)
+            ->setName($freemiumEvent->placeName)
+            ->setAddressPostal($addressPostal);
+    }
+
+    /**
+     * Récupération de la place si définie, sinon on en créer une si un minimum d'information est fournies.
+     */
+    protected function getPlace(FreemiumEvent $freemiumEvent): ?Place
+    {
+        if ($freemiumEvent->place) {
+            return $freemiumEvent->place;
+        } elseif (
+            $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.
+     */
+    protected function getAddressPostal(Place $place): AddressPostal
+    {
+        if ($place->getAddressPostal()) {
+            return $place->getAddressPostal();
+        }
+
+        return new AddressPostal();
+    }
+}

+ 173 - 0
src/Service/ApiResourceBuilder/Freemium/OrganizationMappingBuilder.php

@@ -0,0 +1,173 @@
+<?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\Entity\Organization\TypeOfPractice;
+use App\Enum\Core\ContactPointTypeEnum;
+use App\Enum\Organization\AddressPostalOrganizationTypeEnum;
+
+/**
+ * Class OrganizationMappingBuilder.
+ *
+ * Mapping class that maps information from a FreemiumOrganization to an Organization entity.
+ */
+class OrganizationMappingBuilder
+{
+    /**
+     * 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 type de pratiques
+        $this->updateOrganizationTypeOfPractices($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
+     */
+    protected function mapOrganizationInformations(Organization $organization, FreemiumOrganization $freemiumOrganization): void
+    {
+        $organization->setName($freemiumOrganization->name);
+        $organization->setPrincipalType($freemiumOrganization->principalType);
+        $organization->setLegalStatus($freemiumOrganization->legalStatus);
+        $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);
+    }
+
+    /**
+     * Update the organization's type of practices based on the Freemium organization's type of practices.
+     *
+     * @param Organization         $organization         the organization to update the type of practices for
+     * @param FreemiumOrganization $freemiumOrganization the Freemium organization containing the desired type of practices
+     */
+    protected function updateOrganizationTypeOfPractices(Organization $organization, FreemiumOrganization $freemiumOrganization): void
+    {
+        foreach ($organization->getTypeOfPractices() as $organizationPractice) {
+            $this->removePracticeIfNotInFreemium($organization, $freemiumOrganization, $organizationPractice);
+        }
+
+        foreach ($freemiumOrganization->typeOfPractices as $typeOfPractice) {
+            $this->addPracticeIfNotInOrganization($organization, $typeOfPractice);
+        }
+    }
+
+    /**
+     * Removes a practice from the organization if it is not in the freemium organization.
+     *
+     * @param Organization         $organization         the organization from which to remove the practice
+     * @param FreemiumOrganization $freemiumOrganization the freemium organization used for comparison
+     * @param TypeOfPractice       $organizationPractice the practice to be removed if not in freemium organization
+     */
+    private function removePracticeIfNotInFreemium(Organization $organization, FreemiumOrganization $freemiumOrganization, TypeOfPractice $organizationPractice): void
+    {
+        if (!$freemiumOrganization->typeOfPractices->contains($organizationPractice)) {
+            $organization->removeTypeOfPractice($organizationPractice);
+        }
+    }
+
+    /**
+     * Adds the given TypeOfPractice to the Organization if it is not already associated with it.
+     *
+     * @param Organization   $organization   The organization to add the TypeOfPractice to
+     * @param TypeOfPractice $typeOfPractice The TypeOfPractice to add to the organization
+     */
+    private function addPracticeIfNotInOrganization(Organization $organization, TypeOfPractice $typeOfPractice): void
+    {
+        if (!$organization->getTypeOfPractices()->contains($typeOfPractice)) {
+            $organization->addTypeOfPractice($typeOfPractice);
+        }
+    }
+
+    /**
+     * Mapping des informations de ContactPoint depuis FreemiumOrganization.
+     *
+     * @param ContactPoint         $contactPoint         : objet target
+     * @param FreemiumOrganization $freemiumOrganization : objet source
+     */
+    protected 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
+     */
+    protected 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.
+     */
+    protected 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.
+     */
+    protected 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;
+    }
+}

+ 18 - 2
src/Service/Doctrine/FiltersConfigurationService.php

@@ -26,6 +26,8 @@ class FiltersConfigurationService
      */
     protected ?bool $previousTimeConstraintState = null;
 
+    protected bool $filtersConfigured = false;
+
     public function __construct(
         private EntityManagerInterface $entityManager,
         private DateTimeConstraint $dateTimeConstraint,
@@ -55,6 +57,8 @@ class FiltersConfigurationService
         $activityYearFilter = $this->getFilters()->enable('activity_year_filter');
         $activityYearFilter->setAccessId($accessId);
         $activityYearFilter->setTimeConstraint($this->activityYearConstraint);
+
+        $this->filtersConfigured = true;
     }
 
     /**
@@ -65,8 +69,12 @@ class FiltersConfigurationService
      */
     public function suspendTimeConstraintFilters(): void
     {
+        if (!$this->filtersConfigured) {
+            return;
+        }
+
         if ($this->previousTimeConstraintState !== null) {
-            throw new \RuntimeException('time constraints is already suspended');
+            throw new \RuntimeException('The time constraints are already suspended');
         }
 
         if ($this->timeFiltersAlreadyDisabled()) {
@@ -112,8 +120,16 @@ class FiltersConfigurationService
      */
     public function restoreTimeConstraintFilters(): void
     {
+        if (!$this->filtersConfigured) {
+            return;
+        }
+
         if ($this->previousTimeConstraintState === null) {
-            throw new \RuntimeException('time constraints has not been suspended, can not be restored');
+            throw new \RuntimeException('The time constraints have not been suspended, can not be restored');
+        }
+
+        if ($this->previousTimeConstraintState === false) {
+            return;
         }
 
         $this->enableFilter('date_time_filter');

+ 125 - 0
src/Service/Dolibarr/DolibarrApiService.php

@@ -7,6 +7,7 @@ namespace App\Service\Dolibarr;
 use App\Entity\Organization\Organization;
 use App\Enum\Dolibarr\DolibarrDocTypeEnum;
 use App\Service\Rest\ApiRequestService;
+use App\Service\Utils\DatesUtils;
 use JetBrains\PhpStorm\Pure;
 use Symfony\Component\HttpKernel\Exception\HttpException;
 use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
@@ -55,6 +56,18 @@ class DolibarrApiService extends ApiRequestService
         }
     }
 
+    /**
+     * Get the dolibarr id of an organization.
+     *
+     * @throws \JsonException
+     */
+    public function getSocietyId(int $organizationId): ?int
+    {
+        $society = $this->getSociety($organizationId);
+
+        return $society ? (int) $society['id'] : null;
+    }
+
     /**
      * Get the first active contract for the given dolibarr society.
      *
@@ -279,4 +292,116 @@ class DolibarrApiService extends ApiRequestService
 
         return $this->getJsonContent($route);
     }
+
+    /**
+     * Créé un nouveau contrat Dolibarr.
+     *
+     * @param int $productId L'id du produit dans dolibarr (@see DolibarrUtils::getProductId())
+     * @param int $duration  Durée du contrat (en mois)
+     *
+     * @throws \Exception
+     */
+    public function createContract(
+        int $socId,
+        int $productId,
+        bool $isNewClient = false,
+        int $duration = 12,
+    ): int {
+        $route = 'contracts';
+        $date = DatesUtils::new();
+
+        $product = $this->getJsonContent(
+            "products/$productId"
+        );
+
+        // Evolution (3) ou nouveau client (1)
+        $originVente = $isNewClient ? 1 : 3;
+
+        $body = [
+            'socid' => $socId,
+            'date_contrat' => $date->format('Y-m-d'),
+            'commercial_signature_id' => 8,
+            'commercial_suivi_id' => 8,
+            'statut' => 1,
+            'lines' => [
+                [
+                    'fk_product' => $productId,
+                    'label' => $product['label'],
+                    'desc' => $product['description'],
+                    'qty' => 1,
+                    'subprice' => number_format((float) $product['price'], 2),
+                    'price_base_type' => $product['price_base_type'],
+                    'tva_tx' => $product['tva_tx'],
+                ],
+            ],
+            'array_options' => [
+                'options_ec_amount' => number_format((float) $product['price'], 2),
+                'options_ec_duration_months' => $duration,
+                'options_ec_signature_date' => $date->format('Y-m-d'),
+                'options_ec_effective_date' => $date->format('Y-m-d'),
+                'options_ec_tacit_renewal' => 1,
+                'options_ec_termination_period_months' => 2,
+                'options_ec_billing_due' => 1,
+                'options_ec_billing_frequency' => 4,
+                'options_ec_billing_begin_period' => 1,
+                'options_ec_payment_condition' => 7, // A Livraison
+                'options_ec_payment_mode' => 6, // CB (voir table llx_c_paiement)
+                'options_ec_account' => 1,
+                'options_logicielfact' => 1,
+                'options_versionfact' => 2,
+                'options_2iopen_origvente' => $originVente,
+            ],
+        ];
+
+        return (int) $this->post($route, $body)->getContent();
+    }
+
+    /**
+     * Ajoute une ligne au contrat.
+     *
+     * @param int $duration Durée du contrat (en jours)
+     *
+     * @throws \Exception
+     */
+    public function createContractLine(int $contractId, int $productId, int $duration = 12): int
+    {
+        $route = "contracts/$contractId/lines";
+
+        $date = DatesUtils::new();
+        $endDate = DatesUtils::new()->modify('+'.$duration.' months')->modify('-1 day');
+
+        $product = $this->getJsonContent(
+            "products/$productId"
+        );
+
+        $body = [
+            'fk_product' => $productId,
+            'label' => $product['label'],
+            'desc' => $product['description'],
+            'qty' => 1,
+            'subprice' => number_format((float) $product['price'], 2),
+            'price_base_type' => $product['price_base_type'],
+            'tva_tx' => $product['tva_tx'],
+            'date_start' => $date->format('Y-m-d'),
+            'date_end' => $endDate->format('Y-m-d'),
+        ];
+
+        return (int) $this->post($route, $body)->getContent();
+    }
+
+    /**
+     * Met à jour le produit possédé par la structure dans Dolibarr.
+     */
+    public function updateSocietyProduct(int $socId, string $productName): void
+    {
+        $route = "thirdparties/$socId";
+
+        $body = [
+            'array_options' => [
+                'options_2iopen_software_opentalent' => $productName,
+            ],
+        ];
+
+        $this->put($route, $body);
+    }
 }

+ 140 - 0
src/Service/Dolibarr/DolibarrUtils.php

@@ -0,0 +1,140 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\Dolibarr;
+
+use App\Enum\Organization\SettingsProductEnum;
+use App\Service\Utils\DatesUtils;
+use Doctrine\DBAL\Connection;
+
+/**
+ * Utility class for interacting with Dolibarr ERP/CRM system.
+ *
+ * This class provides various utility methods for:
+ * - Getting product IDs based on contract type and other parameters
+ * - Executing SQL queries on the Dolibarr database
+ * - Updating society commercials
+ * - Adding entries to the commercial action journal
+ * - Getting product names
+ *
+ * It handles the integration between the application and Dolibarr for shop-related operations.
+ */
+class DolibarrUtils
+{
+    public const ARTIST_STANDARD_CMF_PRODUCT_ID = 283;
+    public const ARTIST_PREMIUM_TRIAL_PRODUCT_ID = 598;
+    public const ARTIST_PREMIUM_PRODUCT_ID = 281;
+    public const ARTIST_PREMIUM_CMF_PRODUCT_ID = 282;
+
+    public function __construct(
+        private Connection $dolibarrConnection,
+    ) {
+    }
+
+    /**
+     * Retourne l'id Dolibarr du produit donné, selon le produit possédé et l'appartenance
+     * ou non au réseau CMF.
+     *
+     * @param SettingsProductEnum $contractType Produit concerné (@see SettingsProductEnum)
+     */
+    public function getProductId(
+        SettingsProductEnum $contractType,
+        bool $isTrial = false,
+        bool $isCmf = false,
+    ): int {
+        if ($contractType === SettingsProductEnum::ARTIST_PREMIUM && $isTrial) {
+            return self::ARTIST_PREMIUM_TRIAL_PRODUCT_ID;
+        } elseif ($contractType === SettingsProductEnum::ARTIST_PREMIUM && $isCmf) {
+            return self::ARTIST_PREMIUM_CMF_PRODUCT_ID;
+        } elseif ($contractType === SettingsProductEnum::ARTIST_PREMIUM) {
+            return self::ARTIST_PREMIUM_PRODUCT_ID;
+        } elseif ($contractType === SettingsProductEnum::ARTIST && $isCmf) {
+            return self::ARTIST_STANDARD_CMF_PRODUCT_ID;
+        } else {
+            throw new \InvalidArgumentException('Invalid contract type');
+        }
+    }
+
+    /**
+     * Exécute une requête SQL sur la DB Dolibarr.
+     *
+     * @param string                   $sql    The SQL query to execute
+     * @param array<int|string, mixed> $params The parameters to bind to the query
+     *
+     * @throws \Doctrine\DBAL\Exception
+     */
+    protected function executeQuery(string $sql, array $params = []): void
+    {
+        $this->dolibarrConnection->executeQuery($sql, $params);
+    }
+
+    /**
+     * Remplace le ou les commerciaux actuellement affectés à la société par l'utilisateur 'api'
+     * (pas de solution trouvée via l'API).
+     *
+     * @return void
+     *
+     * @throws \Doctrine\DBAL\Exception
+     */
+    public function updateSocietyCommercialsWithApi(int $societyId)
+    {
+        $apiUserId = 8;
+
+        $this->executeQuery(
+            'DELETE FROM llx_societe_commerciaux WHERE fk_soc = ?',
+            [$societyId]
+        );
+
+        $this->executeQuery(
+            'INSERT INTO llx_societe_commerciaux (fk_soc, fk_user) VALUES (?, ?)',
+            [$societyId, $apiUserId]
+        );
+    }
+
+    /**
+     * Enregistre une entrée dans le journal des actions commercial de la société Dolibarr
+     * (pas de solution trouvée via l'API).
+     *
+     * @throws \Exception
+     */
+    public function addActionComm(int $societyId, string $title, string $message): void
+    {
+        $tz = new \DateTimeZone('Europe/Paris');
+        $now = DatesUtils::new('now', $tz)->format('Y-m-d H:i:s');
+        $apiUserId = 8;
+
+        $sql = "INSERT INTO llx_actioncomm (fk_soc, ref, code, label, note, datep, datep2, datec, fk_user_author, fk_user_mod, fk_user_action, percent) 
+                   VALUES (?, -1, 'AC_OT_ONLINE_STORE', ?, ?, ?, ?, ?, ?, ?, ?, -1)";
+
+        $this->executeQuery($sql, [
+            $societyId,
+            $title,
+            $message,
+            $now,
+            $now,
+            $now,
+            $apiUserId,
+            $apiUserId,
+            $apiUserId,
+        ]);
+    }
+
+    /**
+     * Retourne le nom du produit dans Dolibarr.
+     *
+     * @param SettingsProductEnum $contractType Produit concerné (@see SettingsProductEnum)
+     */
+    public function getDolibarrProductName(SettingsProductEnum $contractType, bool $isTrial = false): ?string
+    {
+        return match ($contractType) {
+            SettingsProductEnum::ARTIST => 'Opentalent Artist',
+            SettingsProductEnum::ARTIST_PREMIUM => $isTrial ? 'Opentalent Artist Premium (Essai)' : 'Opentalent Artist Premium',
+            SettingsProductEnum::SCHOOL => 'Opentalent School',
+            SettingsProductEnum::SCHOOL_PREMIUM => $isTrial ? 'Opentalent School Premium (Essai)' : 'Opentalent School Premium',
+            SettingsProductEnum::MANAGER => 'Opentalent Manager',
+            SettingsProductEnum::MANAGER_PREMIUM => 'Opentalent Manager Premium',
+            default => null,
+        };
+    }
+}

+ 20 - 16
src/Service/Export/Encoder/PdfEncoder.php

@@ -14,18 +14,19 @@ use Dompdf\Options;
  */
 class PdfEncoder implements EncoderInterface
 {
-    protected Options $domPdfOptions;
-    protected Dompdf $dompdf;
+    public function support(string $format): bool
+    {
+        return $format === ExportFormatEnum::PDF->value;
+    }
 
-    public function __construct()
+    protected function getDomPdf(): Dompdf
     {
-        $this->domPdfOptions = new Options();
-        $this->dompdf = new Dompdf();
+        return new Dompdf();
     }
 
-    public function support(string $format): bool
+    protected function getDomPdfOptions(): Options
     {
-        return $format === ExportFormatEnum::PDF->value;
+        return new Options();
     }
 
     /**
@@ -40,16 +41,19 @@ class PdfEncoder implements EncoderInterface
      */
     public function encode(string $html, array $options = []): string
     {
-        $this->domPdfOptions->setIsRemoteEnabled(true);
-        $this->domPdfOptions->setChroot(PathUtils::getProjectDir().'/public');
-        $this->domPdfOptions->setDefaultPaperOrientation('portrait');
-        $this->domPdfOptions->setDefaultPaperSize('A4');
-        $this->domPdfOptions->set($options);
+        $domPdfOptions = $this->getDomPdfOptions();
+        $dompdf = $this->getDomPdf();
+
+        $domPdfOptions->setIsRemoteEnabled(true);
+        $domPdfOptions->setChroot(PathUtils::getProjectDir().'/public');
+        $domPdfOptions->setDefaultPaperOrientation('portrait');
+        $domPdfOptions->setDefaultPaperSize('A4');
+        $domPdfOptions->set($options);
 
-        $this->dompdf->setOptions($this->domPdfOptions);
-        $this->dompdf->loadHtml($html);
-        $this->dompdf->render();
+        $dompdf->setOptions($domPdfOptions);
+        $dompdf->loadHtml($html);
+        $dompdf->render();
 
-        return $this->dompdf->output();
+        return $dompdf->output();
     }
 }

+ 5 - 4
src/Service/Export/LicenceCmfExporter.php

@@ -11,6 +11,7 @@ use App\Enum\Core\FileTypeEnum;
 use App\Repository\Organization\OrganizationRepository;
 use App\Service\Export\Model\LicenceCmf;
 use App\Service\Export\Model\LicenceCmfCollection;
+use App\Service\Utils\StringsUtils;
 
 /**
  * Exporte la licence CMF de la structure ou du ou des access, au format demandé.
@@ -49,12 +50,12 @@ class LicenceCmfExporter extends BaseExporter implements ExporterInterface
         $licenceCmf->setId($organization->getId());
         $licenceCmf->setYear($exportRequest->getYear());
         $licenceCmf->setIsOrganizationLicence($exportRequest instanceof LicenceCmfOrganizationER);
-        $licenceCmf->setOrganizationName($organization->getName());
+        $licenceCmf->setOrganizationName(StringsUtils::elide($organization->getName(), 64));
         $licenceCmf->setOrganizationIdentifier($organization->getIdentifier());
 
         $parentFederation = $organization->getNetworkOrganizations()->get(0)?->getParent();
         if ($parentFederation !== null) {
-            $licenceCmf->setFederationName($parentFederation->getName());
+            $licenceCmf->setFederationName(StringsUtils::elide($parentFederation->getName(), 64));
         }
 
         $licenceCmf->setColor(
@@ -72,8 +73,8 @@ class LicenceCmfExporter extends BaseExporter implements ExporterInterface
             if ($president !== null) {
                 $licenceCmf->setPersonId($president->getId());
                 $licenceCmf->setPersonGender($president->getGender() ?? null);
-                $licenceCmf->setPersonFirstName($president->getGivenName());
-                $licenceCmf->setPersonLastName($president->getName());
+                $licenceCmf->setPersonFirstName(StringsUtils::elide($president->getGivenName(), 64));
+                $licenceCmf->setPersonLastName(StringsUtils::elide($president->getName(), 64));
             }
         }
 

+ 1 - 1
src/Service/File/Storage/ApiLegacyStorage.php

@@ -44,7 +44,7 @@ class ApiLegacyStorage implements FileStorageInterface
      */
     public function getImageUrl(File $file, string $size, bool $relativePath, bool $uncropped = false): string
     {
-        $url = sprintf('api/files/%s/download/%s?relativePath=1', $file->getId(), $size);
+        $url = sprintf('api/public/files/%s/download/%s?relativePath=1', $file->getId(), $size);
 
         if ($uncropped) {
             $url .= '&noCache=true';

+ 3 - 1
src/Service/Mailer/Builder/AbstractBuilder.php

@@ -64,7 +64,9 @@ class AbstractBuilder implements AbstractBuilderInterface
      */
     public function render(string $template, array $context): string
     {
-        return $this->twig->render(sprintf('@templates/emails/%s.html.twig', $template), $context);
+        $templatePath = sprintf('@templates/emails/%s.html.twig', $template);
+
+        return $this->twig->render($templatePath, $context);
     }
 
     /**

+ 58 - 0
src/Service/Mailer/Builder/Shop/NewStructureArtistPremium/ConfirmationToRepresentativeBuilder.php

@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\Mailer\Builder\Shop\NewStructureArtistPremium;
+
+use App\Entity\Access\Access;
+use App\Enum\Core\EmailSendingTypeEnum;
+use App\Service\Mailer\Builder\AbstractBuilder;
+use App\Service\Mailer\Builder\BuilderInterface;
+use App\Service\Mailer\Email;
+use App\Service\Mailer\Model\MailerModelInterface;
+use App\Service\Mailer\Model\Shop\NewStructureArtistPremium\ConfirmationToRepresentativeModel;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\ORM\EntityManagerInterface;
+
+/**
+ * Classe NewStructureArtistPremiumTrialRequestAccountCreationBuilder qui est chargé de construire l'Email
+ * d'accès au compte essai Opentalent Artist Premium.
+ */
+class ConfirmationToRepresentativeBuilder extends AbstractBuilder implements BuilderInterface
+{
+    public function __construct(
+        private readonly EntityManagerInterface $entityManager,
+        private readonly string $opentalentNoReplyEmailAddress,
+    ) {
+    }
+
+    public function support(MailerModelInterface $mailerModel): bool
+    {
+        return $mailerModel instanceof ConfirmationToRepresentativeModel;
+    }
+
+    /**
+     * @param ConfirmationToRepresentativeModel $mailerModel
+     */
+    public function build(MailerModelInterface $mailerModel): ArrayCollection
+    {
+        $author = $this->entityManager->getRepository(Access::class)->find($mailerModel->getSenderId());
+
+        $context = [
+            'mailerModel' => $mailerModel,
+        ];
+
+        $content = $this->render('shop/NewStructureArtistPremium/confirmation-to-representative', $context);
+
+        $email = (new Email())
+            ->setEmailEntity($this->buildEmailEntity('Accédez à votre compte essai Opentalent Artist Premium', $author, $content))
+            ->setContent($content)
+            ->setFrom($this->opentalentNoReplyEmailAddress)
+            ->setFromName('Opentalent - Agenda & Logiciels Culturels');
+
+        // Add recipient as a string (direct email address)
+        $this->addRecipient($email, $mailerModel->getTrialRequest()->getRepresentativeEmail(), EmailSendingTypeEnum::TO);
+
+        return new ArrayCollection([$email]);
+    }
+}

+ 58 - 0
src/Service/Mailer/Builder/Shop/NewStructureArtistPremium/NotificationToSalesAdminBuilder.php

@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\Mailer\Builder\Shop\NewStructureArtistPremium;
+
+use App\Entity\Access\Access;
+use App\Enum\Core\EmailSendingTypeEnum;
+use App\Service\Mailer\Builder\AbstractBuilder;
+use App\Service\Mailer\Builder\BuilderInterface;
+use App\Service\Mailer\Email;
+use App\Service\Mailer\Model\MailerModelInterface;
+use App\Service\Mailer\Model\Shop\NewStructureArtistPremium\NotificationToSalesAdminModel;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\ORM\EntityManagerInterface;
+
+/**
+ * Classe NewStructureArtistPremiumTrialRequestSalesAdminBuilder qui est chargé de construire l'Email
+ * d'information à l'administration des ventes concernant une demande d'essai artist premium pour une nouvelle structure.
+ */
+class NotificationToSalesAdminBuilder extends AbstractBuilder implements BuilderInterface
+{
+    public function __construct(
+        private readonly EntityManagerInterface $entityManager,
+        private readonly string $opentalentNoReplyEmailAddress,
+    ) {
+    }
+
+    public function support(MailerModelInterface $mailerModel): bool
+    {
+        return $mailerModel instanceof NotificationToSalesAdminModel;
+    }
+
+    /**
+     * @param NotificationToSalesAdminModel $mailerModel
+     */
+    public function build(MailerModelInterface $mailerModel): ArrayCollection
+    {
+        $author = $this->entityManager->getRepository(Access::class)->find($mailerModel->getSenderId());
+
+        $context = [
+            'trialRequest' => $mailerModel->getTrialRequest(),
+        ];
+
+        $content = $this->render('shop/NewStructureArtistPremium/notification-to-sales-admin', $context);
+
+        $email = (new Email())
+            ->setEmailEntity($this->buildEmailEntity('Nouvelle demande d\'essai Artist Premium', $author, $content))
+            ->setContent($content)
+            ->setFrom($this->opentalentNoReplyEmailAddress)
+            ->setFromName('Opentalent - Agenda & Logiciels Culturels');
+
+        // Add recipient as a string (direct email address)
+        $this->addRecipient($email, 'adv@opentalent.fr', EmailSendingTypeEnum::TO);
+
+        return new ArrayCollection([$email]);
+    }
+}

+ 57 - 0
src/Service/Mailer/Builder/Shop/TokenValidationBuilder.php

@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\Mailer\Builder\Shop;
+
+use App\Entity\Access\Access;
+use App\Enum\Core\EmailSendingTypeEnum;
+use App\Service\Mailer\Builder\AbstractBuilder;
+use App\Service\Mailer\Builder\BuilderInterface;
+use App\Service\Mailer\Email;
+use App\Service\Mailer\Model\MailerModelInterface;
+use App\Service\Mailer\Model\Shop\TokenValidationModel;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\ORM\EntityManagerInterface;
+
+/**
+ * Classe NewStructureTrialRequestValidationBuilder qui est chargé de construire l'Email de validation d'une demande d'essai de nouvelle structure.
+ */
+class TokenValidationBuilder extends AbstractBuilder implements BuilderInterface
+{
+    public function __construct(
+        private readonly EntityManagerInterface $entityManager,
+        private readonly string $opentalentNoReplyEmailAddress,
+    ) {
+    }
+
+    public function support(MailerModelInterface $mailerModel): bool
+    {
+        return $mailerModel instanceof TokenValidationModel;
+    }
+
+    /**
+     * @param TokenValidationModel $mailerModel
+     */
+    public function build(MailerModelInterface $mailerModel): ArrayCollection
+    {
+        $author = $this->entityManager->getRepository(Access::class)->find($mailerModel->getSenderId());
+
+        $context = [
+            'mailerModel' => $mailerModel,
+        ];
+
+        $content = $this->render('shop/token-validation', $context);
+
+        $email = (new Email())
+            ->setEmailEntity($this->buildEmailEntity('Validation de votre demande d\'essai', $author, $content))
+            ->setContent($content)
+            ->setFrom($this->opentalentNoReplyEmailAddress)
+            ->setFromName('Opentalent - Agenda & Logiciels Culturels');
+
+        // Add recipient as a string (direct email address)
+        $this->addRecipient($email, $mailerModel->getRepresentativeEmail(), EmailSendingTypeEnum::TO);
+
+        return new ArrayCollection([$email]);
+    }
+}

+ 82 - 0
src/Service/Mailer/Model/Shop/NewStructureArtistPremium/ConfirmationToRepresentativeModel.php

@@ -0,0 +1,82 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\Mailer\Model\Shop\NewStructureArtistPremium;
+
+use App\ApiResources\Shop\NewStructureArtistPremiumTrialRequest;
+use App\Service\Mailer\Model\AbstractMailerModel;
+use App\Service\Mailer\Model\MailerModelInterface;
+
+/**
+ * Classe NewStructureArtistPremiumTrialRequestAccountCreationModel qui conserve les données pour construire le mail
+ * d'accès au compte essai Opentalent Artist Premium.
+ */
+class ConfirmationToRepresentativeModel extends AbstractMailerModel implements MailerModelInterface
+{
+    private NewStructureArtistPremiumTrialRequest $trialRequest;
+    private string $accountCreationUrl;
+    private string $faqUrl;
+    private string $adminUsername;
+    private string $adminLoginUrl;
+
+    public function getTrialRequest(): NewStructureArtistPremiumTrialRequest
+    {
+        return $this->trialRequest;
+    }
+
+    public function setTrialRequest(NewStructureArtistPremiumTrialRequest $trialRequest): self
+    {
+        $this->trialRequest = $trialRequest;
+
+        return $this;
+    }
+
+    public function getAccountCreationUrl(): string
+    {
+        return $this->accountCreationUrl;
+    }
+
+    public function setAccountCreationUrl(string $accountCreationUrl): self
+    {
+        $this->accountCreationUrl = $accountCreationUrl;
+
+        return $this;
+    }
+
+    public function getFaqUrl(): string
+    {
+        return $this->faqUrl;
+    }
+
+    public function setFaqUrl(string $faqUrl): self
+    {
+        $this->faqUrl = $faqUrl;
+
+        return $this;
+    }
+
+    public function getAdminUsername(): string
+    {
+        return $this->adminUsername;
+    }
+
+    public function setAdminUsername(string $adminUsername): self
+    {
+        $this->adminUsername = $adminUsername;
+
+        return $this;
+    }
+
+    public function getAdminLoginUrl(): string
+    {
+        return $this->adminLoginUrl;
+    }
+
+    public function setAdminLoginUrl(string $adminLoginUrl): self
+    {
+        $this->adminLoginUrl = $adminLoginUrl;
+
+        return $this;
+    }
+}

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff