Browse Source

Avancement page évent

Vincent 4 months ago
parent
commit
7ac61e3239
29 changed files with 827 additions and 43 deletions
  1. 1 0
      config/opentalent/enum.yaml
  2. 2 0
      config/opentalent/products.yaml
  3. 7 0
      config/packages/api_platform.yaml
  4. 0 0
      src/ApiResource/.gitignore
  5. 37 0
      src/ApiResources/Core/EventCategory.php
  6. 88 9
      src/ApiResources/Freemium/FreemiumEvent.php
  7. 5 6
      src/ApiResources/Freemium/FreemiumOrganization.php
  8. 48 0
      src/ApiResources/Search/PlaceSearchItem.php
  9. 0 1
      src/Doctrine/Access/AdditionalExtension/DateTimeConstraintExtensionAdditional.php
  10. 41 0
      src/Doctrine/Place/CurrentPlaceExtension.php
  11. 77 0
      src/Entity/Booking/Event.php
  12. 1 1
      src/Entity/Core/Categories.php
  13. 15 0
      src/Entity/Core/Familly.php
  14. 15 0
      src/Entity/Core/Gender.php
  15. 16 0
      src/Entity/Core/Subfamilly.php
  16. 15 0
      src/Entity/Place/AbstractPlace.php
  17. 8 2
      src/Entity/Place/Place.php
  18. 18 0
      src/Enum/Booking/PricingEventEnum.php
  19. 2 8
      src/EventListener/OnKernelRequestPreRead.php
  20. 18 0
      src/Repository/Place/PlaceRepository.php
  21. 70 0
      src/Service/ApiResourceBuilder/Freemium/FreemiumEventBuilder.php
  22. 2 1
      src/Service/Utils/EntityUtils.php
  23. 64 0
      src/State/Processor/Freemium/FreemiumEventProcessor.php
  24. 68 0
      src/State/Provider/Core/EventCategoryProvider.php
  25. 34 12
      src/State/Provider/Freemium/FreemiumEventProvider.php
  26. 23 3
      src/State/Provider/ProviderUtils.php
  27. 76 0
      src/State/Provider/Search/PlaceSearchItemProvider.php
  28. 34 0
      src/Validator/Constraints/LessThanField.php
  29. 42 0
      src/Validator/Constraints/LessThanFieldValidator.php

+ 1 - 0
config/opentalent/enum.yaml

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

+ 2 - 0
config/opentalent/products.yaml

@@ -20,6 +20,8 @@ parameters:
           - FreemiumProfile
           - FreemiumEvent
           - AccessProfile
+          - EventCategory
+          - PlaceSearchItem
       Common:
         resources:
           - Preferences

+ 7 - 0
config/packages/api_platform.yaml

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

+ 0 - 0
src/ApiResource/.gitignore


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

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

+ 88 - 9
src/ApiResources/Freemium/FreemiumEvent.php

@@ -4,16 +4,29 @@ declare(strict_types=1);
 
 namespace App\ApiResources\Freemium;
 
-use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
+use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
+use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
 use ApiPlatform\Metadata\ApiFilter;
 use ApiPlatform\Metadata\ApiProperty;
 use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Delete;
 use ApiPlatform\Metadata\Get;
 use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Patch;
+use ApiPlatform\Metadata\Post;
 use App\ApiResources\ApiResourcesInterface;
+use App\Attribute\OrganizationDefaultValue;
 use App\Entity\Booking\Event;
+use App\Entity\Core\Country;
+use App\Entity\Core\File;
+use App\Entity\Organization\Organization;
+use App\Entity\Place\Place;
+use App\Enum\Booking\PricingEventEnum;
+use App\State\Processor\Freemium\FreemiumEventProcessor;
 use App\State\Provider\Freemium\FreemiumEventProvider;
+use Doctrine\Common\Collections\Collection;
 use Symfony\Component\ObjectMapper\Attribute\Map;
+use App\Validator\Constraints as OpentalentAssert;
 
 /**
  * Classe resource contient tous les champs pour la gestion d'un événement pour un profile Freemium.
@@ -23,22 +36,88 @@ use Symfony\Component\ObjectMapper\Attribute\Map;
         new GetCollection(
             uriTemplate: '/freemium/events',
             security: 'is_granted("ROLE_USER_FREEMIUM")',
-            provider: FreemiumEventProvider::class
+        ),
+        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.organizationId == user.getOrganization().getId()))',
-            provider: FreemiumEventProvider::class
+            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
 )]
-#[ApiFilter(SearchFilter::class, properties: ['name' => 'partial'])]
+#[OrganizationDefaultValue(fieldName: 'organization')]
+#[ApiFilter(filterClass: DateFilter::class, properties: ['datetimeStart'])]
+#[ApiFilter(filterClass: OrderFilter::class, properties: ['datetimeStart'], arguments: ['orderParameterName' => 'order'])]
 #[Map(source: Event::class)]
+#[OpentalentAssert\LessThanField(field: 'datetimeStart', comparedTo: 'datetimeEnd')]
 class FreemiumEvent implements ApiResourcesInterface
 {
     #[ApiProperty(identifier: true)]
     public ?int $id = null;
-    #[Map(source: 'organization.id')]
-    public int $organizationName;
-    public ?string $name = null;
+
+    public Organization $organization;
+
+    public string $name;
+
+    public ?\DateTimeInterface $datetimeStart = null;
+
+    public ?\DateTimeInterface $datetimeEnd = null;
+
+    public ?string $description = null;
+
+    public ?File $image = null;
+
+    public ?string $url = null;
+
+    public ?string $urlTicket = null;
+
+    public ?Place $place = null;
+
+    #[Map(source: 'place?.name')]
+    public ?string $placeName = null;
+
+    #[Map(source: 'place?.addressPostal?.streetAddress')]
+    public ?string $streetAddress = null;
+
+    #[Map(source: 'place?.addressPostal?.streetAddressSecond')]
+    public ?string $streetAddressSecond = null;
+
+    #[Map(source: 'place?.addressPostal?.streetAddressThird')]
+    public ?string $streetAddressThird = null;
+
+    #[Map(source: 'place?.addressPostal?.postalCode')]
+    public ?string $postalCode = null;
+
+    #[Map(source: 'place?.addressPostal?.addressCity')]
+    public ?string $addressCity = null;
+
+    #[Map(source: 'place?.addressPostal?.addressCountry')]
+    public ?Country $addressCountry = null;
+
+    #[Map(source: 'place?.addressPostal?.latitude')]
+    public ?float $latitude = null;
+
+    #[Map(source: 'place?.addressPostal?.longitude')]
+    public ?float $longitude = null;
+
+    #[Map(if: false)]
+    public Collection $categories;
+
+    public ?PricingEventEnum $pricing = null;
+
+    public ?float $priceMini = null;
+
+    public ?float $priceMaxi = null;
 }

+ 5 - 6
src/ApiResources/Freemium/FreemiumOrganization.php

@@ -25,15 +25,15 @@ use Symfony\Component\Validator\Constraints as Assert;
     operations: [
         new Get(
             uriTemplate: '/freemium/organization',
-            security: 'is_granted("ROLE_USER_FREEMIUM")',
-            provider: FreemiumOrganizationProvider::class
+            security: 'is_granted("ROLE_USER_FREEMIUM")'
         ),
         new Patch(
             uriTemplate: '/freemium/organization',
-            security: 'is_granted("ROLE_USER_FREEMIUM")',
-            processor: FreemiumOrganizationProcessor::class
+            security: 'is_granted("ROLE_USER_FREEMIUM")'
         ),
-    ]
+    ],
+    provider: FreemiumOrganizationProvider::class,
+    processor: FreemiumOrganizationProcessor::class
 )]
 #[Map(source: Organization::class)]
 class FreemiumOrganization implements ApiResourcesInterface
@@ -92,6 +92,5 @@ class FreemiumOrganization implements ApiResourcesInterface
 
     public bool $portailVisibility = true;
 
-    #[Map(source: 'logo')]
     public ?File $logo = null;
 }

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

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

+ 0 - 1
src/Doctrine/Access/AdditionalExtension/DateTimeConstraintExtensionAdditional.php

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -6,6 +6,7 @@ namespace App\Entity\Place;
 
 use ApiPlatform\Metadata\ApiResource;
 // use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Auditable;
+use ApiPlatform\Metadata\GetCollection;
 use App\Entity\Booking\Course;
 use App\Entity\Booking\EducationalProject;
 use App\Entity\Booking\Event;
@@ -13,6 +14,7 @@ use App\Entity\Booking\Examen;
 use App\Entity\Core\ContactPoint;
 use App\Entity\Organization\Organization;
 use App\Entity\Product\Equipment;
+use App\Repository\Place\PlaceRepository;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
@@ -21,8 +23,12 @@ use Doctrine\ORM\Mapping as ORM;
  * Lieu physique, bâtiment.
  */
 // #[Auditable]
-#[ApiResource(operations: [])]
-#[ORM\Entity]
+#[ApiResource(operations: [
+    new GetCollection(
+        security: 'is_granted(\'ROLE_PLACE\', \'ROLE_USER_FREEMIUM\')'
+    )
+])]
+#[ORM\Entity(repositoryClass: PlaceRepository::class)]
 class Place extends AbstractPlace
 {
     /** @var Collection<int, Event> */

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

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

+ 2 - 8
src/EventListener/OnKernelRequestPreRead.php

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

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

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

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

@@ -0,0 +1,70 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\ApiResourceBuilder\Freemium;
+
+use App\ApiResources\Freemium\FreemiumEvent;
+use App\Entity\Booking\Event;
+use App\Entity\Place\Place;
+use App\Repository\Place\PlaceRepository;
+
+class FreemiumEventBuilder
+{
+    public function __construct(
+        private PlaceRepository $placeRepository
+    )
+    {
+    }
+
+    /**
+     * Mapping des informations.
+     *
+     * @param Event         $event         : objet target
+     * @param FreemiumEvent $freemiumEvent : objet source
+     */
+    public function mapInformations(Event $event, FreemiumEvent $freemiumEvent): void
+    {
+        // Mapping des infos principales
+        $this->mapEventInformations($event, $freemiumEvent);
+      //  $this->mapPlaceInformations($this->getEventPlace($freemiumEvent), $freemiumEvent);
+    }
+
+    /**
+     * Mapping des informations de Event depuis FreemiumEvent.
+     *
+     * @param Event         $event         : objet target
+     * @param FreemiumEvent $freemiumEvent : objet source
+     */
+    private function mapEventInformations(Event $event, FreemiumEvent $freemiumEvent): void
+    {
+        $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->addCategory($freemiumEvent->categories);
+    }
+
+    private function mapPlaceInformations(Place $place, FreemiumEvent $freemiumEvent){
+    }
+
+    /**
+     * @param FreemiumEvent $freemiumEvent
+     * @return Place|array|object[]
+     */
+    private function getEventPlace(FreemiumEvent $freemiumEvent){
+        //@todo: vérifier que l'on ne peux pas mettre n'importe quel ID de place => sécurité avec organisation_id
+        if($freemiumEvent->place){
+            return $this->placeRepository->findBy(['id' => $freemiumEvent->place]);
+        }else{
+            return new Place();
+        }
+    }
+}

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

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

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

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

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

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

+ 34 - 12
src/State/Provider/Freemium/FreemiumEventProvider.php

@@ -6,11 +6,17 @@ namespace App\State\Provider\Freemium;
 
 use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\Pagination\PaginatorInterface;
+use ApiPlatform\State\Pagination\TraversablePaginator;
 use ApiPlatform\State\ProviderInterface;
+use ApiPlatform\Metadata\IriConverterInterface;
 use App\ApiResources\Freemium\FreemiumEvent;
 use App\Entity\Booking\Event;
+use App\Entity\Core\Categories;
+use App\Repository\Booking\EventRepository;
+use App\Service\Doctrine\FiltersConfigurationService;
 use App\State\Provider\ProviderUtils;
-use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Component\ObjectMapper\ObjectMapperInterface;
 
 /**
@@ -21,7 +27,9 @@ final class FreemiumEventProvider implements ProviderInterface
     public function __construct(
         private ProviderUtils $providerUtils,
         private ObjectMapperInterface $objectMapper,
-        private EntityManagerInterface $em,
+        private EventRepository $eventRepository,
+        private FiltersConfigurationService $filtersConfigurationService,
+        private IriConverterInterface $iriConverter
     ) {
     }
 
@@ -32,25 +40,31 @@ final class FreemiumEventProvider implements ProviderInterface
      * @throws \Doctrine\ORM\Exception\ORMException
      * @throws \Doctrine\ORM\OptimisticLockException
      */
-    public function provide(Operation $operation, array $uriVariables = [], array $context = []): \Generator|FreemiumEvent|null
+    public function provide(Operation $operation, array $uriVariables = [], array $context = []): TraversablePaginator|FreemiumEvent|null
     {
         if ($operation instanceof GetCollection) {
             return $this->provideCollection($operation, $context);
         }
-
         return $this->provideItem($uriVariables, $context);
     }
 
     /**
      * @param array<mixed> $context
      */
-    private function provideCollection(Operation $operation, array $context): \Generator
+    private function provideCollection(Operation $operation, array $context): TraversablePaginator
     {
-        $apiPlatformPaginator = $this->providerUtils->applyCollectionExtensionsAndPagination(Event::class, $operation, $context);
+        $this->filtersConfigurationService->suspendTimeConstraintFilters();
+
+        $originalPaginator = $this->providerUtils->applyCollectionExtensionsAndPagination(Event::class, $operation, $context);
 
-        foreach ($apiPlatformPaginator as $event) {
-            yield $this->objectMapper->map($event, FreemiumEvent::class);
+        $mappedItems = [];
+        foreach ($originalPaginator as $item) {
+            $mappedItems[]= $this->objectMapper->map($item, FreemiumEvent::class);
         }
+
+        $this->filtersConfigurationService->restoreTimeConstraintFilters();
+
+        return $this->providerUtils->getTraversablePaginator($mappedItems, $originalPaginator);
     }
 
     /**
@@ -62,11 +76,19 @@ final class FreemiumEventProvider implements ProviderInterface
      */
     private function provideItem(array $uriVariables, array $context): ?FreemiumEvent
     {
-        $event = $this->em->find(Event::class, $uriVariables['id']);
-        if (!$event) {
-            return null;
+        $this->filtersConfigurationService->suspendTimeConstraintFilters();
+        /** @var Event $event */
+        if(empty($event = $this->eventRepository->find($uriVariables['id']))){
+            throw new NotFoundHttpException('event not found');
         }
+        $this->filtersConfigurationService->restoreTimeConstraintFilters();
+
+        $freemiumEvent = $this->objectMapper->map($event, FreemiumEvent::class);
+
+        $freemiumEvent->categories = $event->getCategories()->map(
+            fn(Categories $cat) => $this->iriConverter->getIriFromResource($cat)
+        );
 
-        return $this->objectMapper->map($event, FreemiumEvent::class);
+        return $freemiumEvent;
     }
 }

+ 23 - 3
src/State/Provider/ProviderUtils.php

@@ -5,9 +5,10 @@ declare(strict_types=1);
 namespace App\State\Provider;
 
 use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
-use ApiPlatform\Doctrine\Orm\Paginator as ApiPlatformPaginator;
 use ApiPlatform\Doctrine\Orm\Util\QueryNameGenerator;
 use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\Pagination\Pagination;
+use ApiPlatform\State\Pagination\TraversablePaginator;
 use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator;
 use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
@@ -21,6 +22,7 @@ class ProviderUtils
         private EntityManagerInterface $em,
         #[AutowireIterator(tag: 'api_platform.doctrine.orm.query_extension.collection')]
         private iterable $collectionExtensions,
+        private Pagination $pagination,
     ) {
     }
 
@@ -29,7 +31,7 @@ class ProviderUtils
      *
      * @param array<mixed> $context
      */
-    public function applyCollectionExtensionsAndPagination(string $class, Operation $operation, array $context = []): ApiPlatformPaginator
+    public function applyCollectionExtensionsAndPagination(string $class, Operation $operation, array $context = []): TraversablePaginator
     {
         $qb = $this->em->getRepository($class)->createQueryBuilder('o');
 
@@ -48,7 +50,25 @@ class ProviderUtils
         }
 
         $doctrinePaginator = new DoctrinePaginator($qb);
+        return new TraversablePaginator(
+            $doctrinePaginator,
+            $this->pagination->getPage($context),
+            $this->pagination->getLimit($operation, $context),
+            count($doctrinePaginator) // Nombre total d'éléments pour générer "last", "next", etc.
+        );
+    }
 
-        return new ApiPlatformPaginator($doctrinePaginator);
+    /**
+     * @param $mappedItems
+     * @param $originalPaginator
+     * @return TraversablePaginator
+     */
+    public function getTraversablePaginator(array $mappedItems, TraversablePaginator $originalPaginator) : TraversablePaginator{
+        return new TraversablePaginator(
+            new \ArrayIterator($mappedItems),
+            $originalPaginator->getCurrentPage(),
+            $originalPaginator->getItemsPerPage(),
+            $originalPaginator->getTotalItems()
+        );
     }
 }

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

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

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

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

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

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