Parcourir la source

Merge branch 'feature/parameters' into 'develop'

Page paramètres

See merge request opentalent/ap2i!5
Guffon il y a 3 ans
Parent
commit
5bff7309a9
91 fichiers modifiés avec 2934 ajouts et 514 suppressions
  1. 2 0
      .gitignore
  2. 0 4
      config/api_platform/Access/access.yaml
  3. 3 2
      config/opentalent/enum.yaml
  4. 5 0
      config/opentalent/modulesbyconditions.yaml
  5. 26 66
      config/opentalent/products.yaml
  6. 6 0
      config/packages/dev/messenger.yaml
  7. 3 0
      config/packages/doctrine.yaml
  8. 1 0
      config/packages/messenger.yaml
  9. 0 1
      config/packages/security.yaml
  10. 15 0
      config/services.yaml
  11. 16 0
      src/Annotation/ActivityYearConstraintAware.php
  12. 15 0
      src/Annotation/BillingSettingDefaultValue.php
  13. 81 0
      src/ApiResources/Access/AdminAccess.php
  14. 0 1
      src/ApiResources/Mobyt/MobytUserStatus.php
  15. 16 0
      src/ApiResources/Profile/OrganizationProfile.php
  16. 62 0
      src/DataPersister/Access/AdminAccessDataPersister.php
  17. 51 0
      src/DataPersister/Organization/OrganizationDataPersister.php
  18. 70 0
      src/DataPersister/Organization/ParametersDataPersister.php
  19. 37 0
      src/DataProvider/Access/AdminAccessDataProvider.php
  20. 31 0
      src/Doctrine/AbstractExtension.php
  21. 4 15
      src/Doctrine/Access/CurrentAccessExtension.php
  22. 3 15
      src/Doctrine/Access/CurrentUserPersonalizedListExtension.php
  23. 0 22
      src/Doctrine/Access/Extensions/AdminExtension.php
  24. 35 0
      src/Doctrine/Billing/CurrentResidenceAreaExtension.php
  25. 40 0
      src/Doctrine/Booking/CurrentCoursesExtension.php
  26. 3 15
      src/Doctrine/Core/AllowedAddressPostalExtension.php
  27. 3 15
      src/Doctrine/Core/CurrentNotificationUserExtension.php
  28. 3 15
      src/Doctrine/Core/CurrentUserNotificationExtension.php
  29. 35 0
      src/Doctrine/Education/CurrentCycleExtension.php
  30. 35 0
      src/Doctrine/Education/CurrentEducationNotationConfigExtension.php
  31. 35 0
      src/Doctrine/Education/CurrentEducationTimingExtension.php
  32. 3 15
      src/Doctrine/Network/CurrentNetworkOrganizationExtension.php
  33. 3 15
      src/Doctrine/Organization/CurrentOrganizationAddressPostalExtension.php
  34. 3 15
      src/Doctrine/Organization/CurrentOrganizationArticleExtension.php
  35. 3 15
      src/Doctrine/Organization/CurrentOrganizationExtension.php
  36. 35 1
      src/Entity/Access/Access.php
  37. 39 0
      src/Entity/Billing/BillingSetting.php
  38. 66 0
      src/Entity/Billing/ResidenceArea.php
  39. 88 0
      src/Entity/Booking/Course.php
  40. 1 1
      src/Entity/Core/ContactPoint.php
  41. 15 0
      src/Entity/Core/File.php
  42. 112 0
      src/Entity/Education/Cycle.php
  43. 44 0
      src/Entity/Education/EducationCurriculum.php
  44. 191 0
      src/Entity/Education/EducationNotationConfig.php
  45. 70 0
      src/Entity/Education/EducationTiming.php
  46. 109 2
      src/Entity/Organization/Organization.php
  47. 152 24
      src/Entity/Organization/Parameters.php
  48. 15 0
      src/Enum/Education/AdvancedEducationNotationTypeEnum.php
  49. 19 0
      src/Enum/Education/CycleEnum.php
  50. 11 5
      src/EventListener/DoctrineFilter/DoctrineFilterListener.php
  51. 0 68
      src/EventListener/Organization/OrganizationChangedSubscriber.php
  52. 61 0
      src/Filter/DoctrineFilter/AbstractTimeFilter.php
  53. 60 0
      src/Filter/DoctrineFilter/ActivityYearFilter.php
  54. 2 47
      src/Filter/DoctrineFilter/DateTimeFilter.php
  55. 56 0
      src/Filter/Utils/InFilter.php
  56. 17 0
      src/Message/Command/Parameters/AverageChange.php
  57. 20 0
      src/Message/Handler/Parameters/AverageChangeHandler.php
  58. 44 0
      src/OpenApi/OpenApiFactory.php
  59. 16 0
      src/Repository/Billing/ResidenceAreaRepository.php
  60. 46 0
      src/Repository/Booking/CourseRepository.php
  61. 4 4
      src/Repository/Core/ContactPointRepository.php
  62. 16 0
      src/Repository/Education/CycleRepository.php
  63. 16 0
      src/Repository/Education/EducationCurriculumRepository.php
  64. 16 0
      src/Repository/Education/EducationNotationConfigRepository.php
  65. 16 0
      src/Repository/Education/EducationTimingRepository.php
  66. 1 1
      src/Repository/Organization/OrganizationAddressPostalRepository.php
  67. 0 2
      src/Security/Voter/ModuleVoter.php
  68. 44 0
      src/Service/Access/AdminAccessUtils.php
  69. 16 0
      src/Service/Access/Utils.php
  70. 109 0
      src/Service/Constraint/AbstractTimeConstraintUtils.php
  71. 121 0
      src/Service/Constraint/ActivityYearConstraint.php
  72. 3 102
      src/Service/Constraint/DateTimeConstraint.php
  73. 35 0
      src/Service/Core/ContactPointUtils.php
  74. 21 1
      src/Service/Mobyt/MobytService.php
  75. 29 0
      src/Service/OnChange/Organization/OnOrganizationChange.php
  76. 77 0
      src/Service/OnChange/Organization/OnParametersChange.php
  77. 1 0
      src/Service/Organization/OrganizationProfileCreator.php
  78. 27 0
      src/Service/Organization/Utils.php
  79. 0 1
      src/Service/ServiceIterator/CurrentAccessExtensionIterator.php
  80. 16 0
      src/Service/Utils/EntityUtils.php
  81. 17 0
      src/Validator/Organization/Parameters/MobytCredentials.php
  82. 37 0
      src/Validator/Organization/Parameters/MobytCredentialsValidator.php
  83. 87 0
      tests/Service/Access/AdminAccessUtilsTest.php
  84. 30 1
      tests/Service/Access/UtilsTest.php
  85. 2 2
      tests/Service/Constraint/DateTimeConstraintTest.php
  86. 57 0
      tests/Service/Core/ContactPointUtilsTest.php
  87. 43 0
      tests/Service/Mobyt/MobytServiceTest.php
  88. 39 0
      tests/Service/OnChange/Organization/OnOrganizationChangeTest.php
  89. 92 0
      tests/Service/OnChange/Organization/OnParametersChangeTest.php
  90. 25 0
      tests/Service/Organization/UtilsTest.php
  91. 0 21
      tests/Service/ServiceIterator/CurrentAccessExtensionIteratorTest.php

+ 2 - 0
.gitignore

@@ -35,3 +35,5 @@ symfony.lock
 .phpunit.result.cache
 ###< phpunit/phpunit ###
 /coverage/
+
+composer.lock

+ 0 - 4
config/api_platform/Access/access.yaml

@@ -7,10 +7,6 @@ App\Entity\Access\Access:
       path: '/students'
       security: 'is_granted("ROLE_USERS_VIEW")'
 
-    cget_admin:
-      method: GET
-      path: '/admin'
-
     cget_access_person_ref:
       method: GET
       path: '/access_people'

+ 3 - 2
config/opentalent/enum.yaml

@@ -126,8 +126,9 @@ opentalent:
     education_year: 'App\Enum\Education\YearEnum'
     education_period: 'App\Enum\Education\PeriodEnum'
     education_periodicity: 'App\Enum\Education\PeriodicityEnum'
-    
-  #billing
+    advanced_education_notation: 'App\Enum\Education\AdvancedEducationNotationTypeEnum'
+
+    #billing
     billing_periodicity: 'App\Enum\Billing\PeriodicityEnum'
     billing_type_periodicity: 'App\Enum\Billing\PeriodicityTypeEnum'
     billing_pricingline_adhesion: 'App\Enum\Billing\PricingLineAdhesionEnum'

+ 5 - 0
config/opentalent/modulesbyconditions.yaml

@@ -65,6 +65,11 @@ opentalent:
                     name: App\Service\Network\Utils
                     function: isCMF
         NetworkOrganization:
+            conditions:
+                service:
+                    name: App\Service\Network\Utils
+                    function: isCMF
+        Cotisation:
             conditions:
                 service:
                     name: App\Service\Network\Utils

+ 26 - 66
config/opentalent/products.yaml

@@ -11,11 +11,7 @@ opentalent:
           - City
           - Country
           - Tagg
-        actions:
-          - Export_list
-          - Export_card
-          - Export_report-activity
-          - Export_licence-cmf
+          - Enum
         roles:
           - ROLE_IMPORT
           - ROLE_TAGG
@@ -32,7 +28,12 @@ opentalent:
           - PlaceRepair
           - Room
           - OrganizationAddressPostal
+          - GpsCoordinate
           - Organization
+          - Siret
+          - Parameters
+          - AdminAccess
+          - BankAccount
           - OrganizationHoliday
           - TypeOfPractice
           - Activity
@@ -48,13 +49,10 @@ opentalent:
           - OrganizationFunction
           - FunctionType
           - PersonAddressPostal
+          - GpsCoordinate
           - PersonActivity
           - PersonHoliday
           - OrganizationResponsability
-        actions:
-          - CreateAccounts
-          - DeleteAccounts
-          - SharingContact
         roles:
           - ROLE_USERS
           - ROLE_ACCOUNTS
@@ -70,8 +68,6 @@ opentalent:
           - EducationStudent
           - Course
           - Education
-        actions:
-          - CreateOwnPresenceAttendance
 
       Medals:
         entities:
@@ -89,13 +85,9 @@ opentalent:
         entities:
           - Message
           - Email
-          - Sms
           - ReportMessage
-        actions:
-          - Rulerz
         roles:
           - ROLE_EMAILS
-          - ROLE_TEXTO
 
       MessagesAdvanced:
         entities:
@@ -103,6 +95,13 @@ opentalent:
         roles:
           - ROLE_MAILS
 
+      Sms:
+        entities:
+          - Sms
+          - MobytUserStatus
+        roles:
+          - ROLE_TEXTO
+
       TemplateMessages:
         entities:
           - TemplateSystem
@@ -126,10 +125,6 @@ opentalent:
             - Categories
             - EventReport
             - AttendanceBooking
-          actions:
-            - Rulerz
-            - EventInvitation
-            - Export_presence-event
           roles:
             - ROLE_EVENTS
 
@@ -139,9 +134,6 @@ opentalent:
             - Work
             - WorkByUser
             - EducationStudent
-          actions:
-            - Export_presence-course
-            - DuplicateCourses
           roles:
             - ROLE_COURSES
 
@@ -149,9 +141,6 @@ opentalent:
           entities:
             - Examen
             - Jury
-          actions:
-            - ExamenConvocation
-            - Export_presence-examen
           roles:
             - ROLE_EXAMENS
 
@@ -159,8 +148,6 @@ opentalent:
           entities:
             - EducationalProject
             - EducationalProjectPublic
-          actions:
-            - Export_road-map
           roles:
             - ROLE_EDUCATIONALPROJECTS
 
@@ -179,9 +166,6 @@ opentalent:
             - EquipmentRepair
             - EquipmentList
             - EquipmentComposition
-          actions:
-            - EquipmentLoanReminder
-            - Export_equipment-availability-form
           roles:
             - ROLE_EQUIPMENTS
 
@@ -196,9 +180,7 @@ opentalent:
             - ExamenConvocation
             - Education
             - Cycle
-          actions:
-            - CycleUpdating
-            - Export_report-card
+            - EducationTiming
           roles:
             - ROLE_PEDAGOGICS_ADMINISTRATION
 
@@ -207,8 +189,6 @@ opentalent:
             - EducationStudent
           roles:
             - ROLE_PEDAGOGICS_SEIZURE
-          actions:
-            - EvaluateStudents
 
       AdvancedEducationNotation:
         entities:
@@ -224,23 +204,10 @@ opentalent:
             - BillAccounting
             - BillPayment
             - Bill
+            - BillingSetting
             - BillLine
             - AccessPayer
             - AccessIntangible
-          actions:
-            - BillingEdition
-            - CreditEdition
-            - Rulerz
-            - Export_unpaid-bill
-            - Export_unpaid-bill-email
-            - Export_billing
-            - Export_all-bill
-            - Export_paid-bill
-            - Export_paid-bill-email
-            - Export_bill-billlines
-            - Export_bill-payment-receipt
-            - Export_bill-payment-detail
-            - Export_all-bill-email
           roles :
             - ROLE_BILLINGS_ADMINISTRATION
             - ROLE_BILLINGS_SEIZURE
@@ -248,9 +215,6 @@ opentalent:
       Pes:
           entities:
             - Pes
-          actions:
-            - Export_pes
-            - Export_sepa-debit-mandate
 
       IEL:
           entities:
@@ -259,26 +223,16 @@ opentalent:
             - AccessTmp
             - EducationStudentWish
             - OnlineRegistrationSettings
-          actions:
-            - SendOnlineRegistration
-            - ValidationRegistration
-            - IelProvisionalRegistration
           roles:
             - ROLE_ONLINEREGISTRATION_ADMINISTRATION
 
       BergerLevrault:
           entities:
             - BergerLevrault
-          actions:
-            - Export_berger-levrault
-            - Export_sepa-debit-mandate
 
       Jvs:
           entities:
             - Jvs
-          actions:
-            - Export_jvs
-            - Export_sepa-debit-mandate
 
       Website:
           entities: ~
@@ -299,10 +253,14 @@ opentalent:
             - Network
           roles:
             - ROLE_NETWORK
-          actions:
-            - GenerateCredentials
-            - ChangeEmailAdmin
-            - TakeOutOrganization
+
+      Cotisation:
+        entities:
+          - Cotisation
+
+      Dolibarr:
+        entities:
+          - DolibarrAccount
 
     products:
       artist:
@@ -320,6 +278,8 @@ opentalent:
           - Messages
           - Tagg
           - Statistic
+          - Cotisation
+          - Dolibarr
 
       artist_premium:
         extend: artist

+ 6 - 0
config/packages/dev/messenger.yaml

@@ -0,0 +1,6 @@
+framework:
+    messenger:
+        transports:
+            # https://symfony.com/doc/current/messenger.html#transport-configuration
+            async: 'sync://'
+            failed: 'sync://'

+ 3 - 0
config/packages/doctrine.yaml

@@ -25,6 +25,9 @@ doctrine:
                     date_time_filter:
                         class: App\Filter\DoctrineFilter\DateTimeFilter
                         enabled: true
+                    activity_year_filter:
+                        class: App\Filter\DoctrineFilter\ActivityYearFilter
+                        enabled: true
                 connection: default
                 auto_mapping: true
                 mappings:

+ 1 - 0
config/packages/messenger.yaml

@@ -12,3 +12,4 @@ framework:
         routing:
             # Route your messages to the transports
             'App\Message\Command\Export': async
+            'App\Message\Command\Parameters\AverageChange': async

+ 0 - 1
config/packages/security.yaml

@@ -167,5 +167,4 @@ security:
     # Easy way to control access for large sections of your site
     # Note: Only the *first* access control that matches will be used
     access_control:
-        - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
         - { path: ^/api, roles: IS_HAVING_MODULE }

+ 15 - 0
config/services.yaml

@@ -28,6 +28,11 @@ services:
             - '../src/Kernel.php'
             - '../src/Tests/'
 
+    App\OpenApi\OpenApiFactory:
+        decorates: 'api_platform.openapi.factory'
+        arguments: [ '@App\OpenApi\OpenApiFactory.inner' ]
+        autoconfigure: false
+
     App\Service\Cotisation\Utils:
         public: true
 
@@ -85,3 +90,13 @@ services:
     App\EventListener\DoctrineFilter\DoctrineFilterListener:
         tags:
             - { name: kernel.event_listener, event: kernel.request }
+
+    #########################################
+    ##  DATAPERSISTER ##
+    App\DataPersister\Organization\ParametersDataPersister:
+        bind:
+            $decorated: '@api_platform.doctrine.orm.data_persister'
+
+    App\DataPersister\Organization\OrganizationDataPersister:
+        bind:
+            $decorated: '@api_platform.doctrine.orm.data_persister'

+ 16 - 0
src/Annotation/ActivityYearConstraintAware.php

@@ -0,0 +1,16 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Annotation;
+
+use Attribute;
+
+/**
+ * Classe ActivityYearConstraintAware qui gère l'annotation pour le Doctrine filter
+ */
+#[Attribute(Attribute::TARGET_CLASS)]
+final class ActivityYearConstraintAware
+{
+    public string $startYearFieldName;
+    public string $endYearFieldName;
+}

+ 15 - 0
src/Annotation/BillingSettingDefaultValue.php

@@ -0,0 +1,15 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Annotation;
+
+use Attribute;
+
+/**
+ * Classe BillingSettingDefaultValue qui gère l'annotation pour mettre le billing setting de l'organization comme valeur par défaut
+ */
+#[Attribute(Attribute::TARGET_CLASS)]
+final class BillingSettingDefaultValue
+{
+    public string $fieldName;
+}

+ 81 - 0
src/ApiResources/Access/AdminAccess.php

@@ -0,0 +1,81 @@
+<?php
+declare(strict_types=1);
+
+namespace App\ApiResources\Access;
+
+use ApiPlatform\Core\Annotation\ApiProperty;
+use ApiPlatform\Core\Annotation\ApiResource;
+use App\ApiResources\ApiResourcesInterface;
+use JetBrains\PhpStorm\Pure;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * Classe resource qui contient les champs d'un compte admin
+ */
+#[ApiResource(
+    collectionOperations:[],
+    itemOperations: [
+        'get' => [
+            'method' => 'GET',
+            'path' => '/admin/{id}',
+            'defaults' => ['id' => 0]
+        ],
+        'put' => [
+            'method' => 'PUT',
+            'path' => '/admin/{id}',
+            'defaults' => ['id' => 0]
+        ]
+    ]
+)]
+class AdminAccess implements ApiResourcesInterface
+{
+    #[ApiProperty(identifier: true)]
+    public ?int $id = null;
+
+    private ?string $username = null;
+
+    #[Assert\Email(message: 'invalid-email-format', mode: 'strict')]
+    private ?string $email = null;
+
+
+    #[Pure] public function __construct()
+    {
+    }
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function setId(?int $id): self
+    {
+        $this->id = $id;
+
+        return $this;
+    }
+
+    public function getUsername(): ?string
+    {
+        return $this->username;
+    }
+
+    public function setUsername(?string $username): self
+    {
+        $this->username = $username;
+
+        return $this;
+    }
+
+
+    public function getEmail(): ?string
+    {
+        return $this->email;
+    }
+
+    public function setEmail(?string $email): self
+    {
+        $this->email = $email;
+
+        return $this;
+    }
+}

+ 0 - 1
src/ApiResources/Mobyt/MobytUserStatus.php

@@ -6,7 +6,6 @@ namespace App\ApiResources\Mobyt;
 use ApiPlatform\Core\Annotation\ApiProperty;
 use ApiPlatform\Core\Annotation\ApiResource;
 use App\ApiResources\ApiResourcesInterface;
-use Symfony\Component\Serializer\Annotation\Groups;
 
 /**
  * Statut de l'utilisateur Mobyt correspondant à l'organization donnée en paramètre

+ 16 - 0
src/ApiResources/Profile/OrganizationProfile.php

@@ -57,6 +57,9 @@ class OrganizationProfile implements ApiResourcesInterface
     #[Groups('access_profile_read')]
     private ?int $currentYear = null;
 
+    #[Groups('access_profile_read')]
+    private ?int $parametersId = null;
+
     public function getId(): ?int
     {
         return $this->id;
@@ -200,4 +203,17 @@ class OrganizationProfile implements ApiResourcesInterface
 
         return $this;
     }
+
+
+    public function getParametersId(): ?int
+    {
+        return $this->parametersId;
+    }
+
+    public function setParametersId(?int $parametersId): self
+    {
+        $this->parametersId = $parametersId;
+
+        return $this;
+    }
 }

+ 62 - 0
src/DataPersister/Access/AdminAccessDataPersister.php

@@ -0,0 +1,62 @@
+<?php
+declare(strict_types=1);
+
+namespace App\DataPersister\Access;
+
+use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
+use App\Entity\Access\Access;
+use App\Service\Access\Utils;
+use App\Service\Core\ContactPointUtils;
+use Exception;
+use App\ApiResources\Access\AdminAccess;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\Security\Core\Security;
+
+
+/**
+ * Classe AdminAccessDataPersister qui est un custom dataPersister gérant la resource AdminAccess
+ */
+class AdminAccessDataPersister implements ContextAwareDataPersisterInterface
+{
+    public function __construct(
+        private Security $security,
+        private EntityManagerInterface $entityManager,
+        private Utils $accessUtils,
+        private ContactPointUtils $contactPointUtils
+    )
+    { }
+
+    public function supports($data, array $context = []): bool
+    {
+        return $data instanceof AdminAccess;
+    }
+
+    /**
+     * @param AdminAccess $data
+     * @param array $context
+     * @return void|null
+     */
+    public function persist($data, array $context = [])
+    {
+        /** @var Access $access */
+        $access = $this->security->getUser();
+
+        $administrator = $this->accessUtils->getAdminAccess($access->getOrganization());
+        if(!$administrator){
+            throw new Exception('administrator_not_found', 404);
+        }
+
+        $contactPoint = $this->contactPointUtils->getPersonContactPointPrincipal($administrator);
+        if(!$contactPoint){
+            throw new Exception('administrator_contact_point_not_found', 404);
+        }
+
+        $contactPoint->setEmail($data->getEmail());
+        $this->entityManager->flush();
+    }
+
+    public function remove($data, array $context = [])
+    {
+        throw new Exception('not supported', 500);
+    }
+}

+ 51 - 0
src/DataPersister/Organization/OrganizationDataPersister.php

@@ -0,0 +1,51 @@
+<?php
+declare(strict_types=1);
+
+namespace App\DataPersister\Organization;
+
+use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
+use App\Entity\Organization\Organization;
+use App\Service\OnChange\Organization\OnOrganizationChange;
+use Exception;
+
+/**
+ * Classe OrganizationDataPersister qui est un custom dataPersister gérant la resource Organization
+ */
+class OrganizationDataPersister implements ContextAwareDataPersisterInterface
+{
+    public function __construct(
+        private ContextAwareDataPersisterInterface $decorated,
+        private OnOrganizationChange $onOrganizationChange
+    )
+    { }
+
+    public function supports($data, array $context = []): bool
+    {
+        return $data instanceof Organization;
+    }
+
+    /**
+     * @param Organization $data
+     * @param array $context
+     * @return object|void
+     */
+    public function persist($data, array $context = [])
+    {
+        $this->prePersist($context['previous_data'], $data);
+
+        $result = $this->decorated->persist($data, $context);
+
+        return $result;
+    }
+
+    public function remove($data, array $context = [])
+    {
+        throw new Exception('not supported', 500);
+    }
+
+    public function prePersist(Organization $previousOrganization, Organization $organization): void{
+        if($previousOrganization->getLegalStatus() != $organization->getLegalStatus()){
+            $this->onOrganizationChange->onLegalStatusChange($organization);
+        }
+    }
+}

+ 70 - 0
src/DataPersister/Organization/ParametersDataPersister.php

@@ -0,0 +1,70 @@
+<?php
+declare(strict_types=1);
+
+namespace App\DataPersister\Organization;
+
+use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
+use App\Entity\Organization\Parameters;
+use App\Message\Command\Parameters\AverageChange;
+use App\Service\OnChange\Organization\OnParametersChange;
+use Exception;
+use Symfony\Component\Messenger\MessageBusInterface;
+
+/**
+ * Classe ParametersDataPersister qui est un custom dataPersister gérant la resource Parameters
+ */
+class ParametersDataPersister implements ContextAwareDataPersisterInterface
+{
+    public function __construct(
+        private ContextAwareDataPersisterInterface $decorated,
+        private MessageBusInterface $messageBus,
+        private OnParametersChange $onParametersChange
+    )
+    { }
+
+    public function supports($data, array $context = []): bool
+    {
+        return $data instanceof Parameters;
+    }
+
+    /**
+     * @param Parameters $data
+     * @param array $context
+     * @return object|void
+     */
+    public function persist($data, array $context = [])
+    {
+        $this->prePersist($context['previous_data'], $data);
+
+        $result = $this->decorated->persist($data, $context);
+
+        $this->postPersist($context['previous_data'], $data);
+
+        return $result;
+    }
+
+    public function remove($data, array $context = [])
+    {
+        throw new Exception('not supported', 500);
+    }
+
+    public function prePersist(Parameters $previousParameters, Parameters $parameters): void{
+        if($previousParameters->getAdvancedEducationNotationType() != $parameters->getAdvancedEducationNotationType()){
+            $this->onParametersChange->onAdvancedEducationNotationTypeChange($parameters);
+        }
+
+        //La date de début d'activité change
+        if($previousParameters->getMusicalDate() != $parameters->getMusicalDate()){
+            $this->onParametersChange->onMusicalDateChange($parameters->getOrganization(), $previousParameters->getMusicalDate());
+        }
+    }
+
+    public function postPersist(Parameters $previousParameters, Parameters $parameters): void{
+        //La note maximale du suivi pédagogique change
+        if($previousParameters->getAverage() != $parameters->getAverage()){
+            $this->messageBus->dispatch(
+                new AverageChange($parameters->getId())
+            );
+        }
+    }
+}

+ 37 - 0
src/DataProvider/Access/AdminAccessDataProvider.php

@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\DataProvider\Access;
+
+use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
+use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
+use App\ApiResources\Access\AdminAccess;
+use App\Entity\Access\Access;
+use App\Service\Access\AdminAccessUtils;
+use Symfony\Component\Security\Core\Security;
+
+/**
+ * Class AccessProfileDataProvider : custom provider pour la resource AdminAccess
+ * @package App\DataProvider\Access
+ */
+final class AdminAccessDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface
+{
+    public function __construct(
+        private Security $security,
+        private AdminAccessUtils $adminAccessUtils
+    )
+    { }
+
+    public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
+    {
+        return AdminAccess::class === $resourceClass;
+    }
+
+    public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?AdminAccess
+    {
+        /** @var Access $access */
+        $access = $this->security->getUser();
+        return $this->adminAccessUtils->getAdminAccess($access->getOrganization());
+    }
+}

+ 31 - 0
src/Doctrine/AbstractExtension.php

@@ -0,0 +1,31 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Doctrine;
+
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use Doctrine\ORM\QueryBuilder;
+use Exception;
+
+/**
+ * Classe ... qui ...
+ */
+abstract class AbstractExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+{
+    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
+    {
+        $this->addWhere($queryBuilder, $resourceClass, $operationName);
+    }
+
+    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
+    {
+        $this->addWhere($queryBuilder, $resourceClass, $operationName);
+    }
+
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    {
+        throw new Exception('need override addWhere function', 500);
+    }
+}

+ 4 - 15
src/Doctrine/Access/CurrentAccessExtension.php

@@ -3,9 +3,7 @@ declare(strict_types=1);
 
 namespace App\Doctrine\Access;
 
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use App\Doctrine\AbstractExtension;
 use App\Entity\Access\Access;
 use App\Service\ServiceIterator\CurrentAccessExtensionIterator;
 use Doctrine\ORM\QueryBuilder;
@@ -15,7 +13,7 @@ use Symfony\Component\Security\Core\Security;
  * Class CurrentAccessExtension : Filtre de sécurité par défaut pour une resource Access
  * @package App\Doctrine\Access
  */
-final class CurrentAccessExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentAccessExtension extends AbstractExtension
 {
     public function __construct(
         private Security $security,
@@ -23,21 +21,12 @@ final class CurrentAccessExtension implements QueryCollectionExtensionInterface,
     )
     { }
 
-    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
     {
         if (Access::class !== $resourceClass) {
             return;
         }
+
         /** @var Access $currentUser */
         $currentUser = $this->security->getUser();
         $rootAlias = $queryBuilder->getRootAliases()[0];

+ 3 - 15
src/Doctrine/Access/CurrentUserPersonalizedListExtension.php

@@ -3,9 +3,7 @@ declare(strict_types=1);
 
 namespace App\Doctrine\Access;
 
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use App\Doctrine\AbstractExtension;
 use App\Entity\Access\PersonalizedList;
 use Doctrine\ORM\QueryBuilder;
 use Symfony\Component\Security\Core\Security;
@@ -14,22 +12,12 @@ use Symfony\Component\Security\Core\Security;
  * Class CurrentUserPersonalizedListExtension : Filtre de sécurité par défaut pour une resource PersonalizedList
  * @package App\Doctrine\Access
  */
-final class CurrentUserPersonalizedListExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentUserPersonalizedListExtension extends AbstractExtension
 {
     public function __construct(private Security $security)
     { }
 
-    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
     {
         if (PersonalizedList::class !== $resourceClass) {
             return;

+ 0 - 22
src/Doctrine/Access/Extensions/AdminExtension.php

@@ -1,22 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\Doctrine\Access\Extensions;
-
-use App\Doctrine\Access\AccessExtensionInterface;
-use Doctrine\ORM\QueryBuilder;
-
-class AdminExtension implements AccessExtensionInterface {
-    public function support(string $name): bool
-    {
-        return $name === 'cget_admin';
-    }
-
-    public function addWhere(QueryBuilder $queryBuilder)
-    {
-        $rootAlias = $queryBuilder->getRootAliases()[0];
-        $queryBuilder
-            ->andWhere(sprintf('%s.adminAccess = :adminAccess', $rootAlias))
-            ->setParameter('adminAccess', true);
-    }
-}

+ 35 - 0
src/Doctrine/Billing/CurrentResidenceAreaExtension.php

@@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Doctrine\Billing;
+
+use App\Doctrine\AbstractExtension;
+use App\Entity\Access\Access;
+use App\Entity\Billing\ResidenceArea;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\Security\Core\Security;
+
+/**
+ * Class CurrentResidenceAreaExtension : Filtre de sécurité par défaut pour une resource ResidenceArea
+ * @package App\Doctrine\Core
+ */
+final class CurrentResidenceAreaExtension extends AbstractExtension
+{
+    public function __construct(private Security $security)
+    { }
+
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    {
+        if (ResidenceArea::class !== $resourceClass) {
+            return;
+        }
+
+        /** @var Access $currentUser */
+        $currentUser = $this->security->getUser();
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder
+            ->andWhere(sprintf('%s.billingSetting = :billingSetting', $rootAlias))
+            ->setParameter('billingSetting', $currentUser->getOrganization()->getBillingSetting())
+        ;
+    }
+}

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

@@ -0,0 +1,40 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Doctrine\Booking;
+
+use App\Doctrine\AbstractExtension;
+use App\Entity\Access\Access;
+use App\Entity\Booking\Course;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\Security\Core\Security;
+
+/**
+ * Class CurrentCoursesExtension : Filtre de sécurité par défaut pour une resource Course
+ * @package App\Doctrine\Core
+ */
+final class CurrentCoursesExtension extends AbstractExtension
+{
+    public function __construct(private Security $security)
+    { }
+
+    /**
+     * @todo : A la suite de la migration, il faut supprimer le where avec le discr.
+     */
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    {
+        if (Course::class !== $resourceClass) {
+            return;
+        }
+
+        /** @var Access $currentUser */
+        $currentUser = $this->security->getUser();
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder
+            ->andWhere(sprintf('%s.discr = :discr', $rootAlias))
+            ->andWhere(sprintf('%s.organization = :organization', $rootAlias))
+            ->setParameter('discr', 'course')
+            ->setParameter('organization', $currentUser->getOrganization())
+        ;
+    }
+}

+ 3 - 15
src/Doctrine/Core/AllowedAddressPostalExtension.php

@@ -3,9 +3,7 @@ declare(strict_types=1);
 
 namespace App\Doctrine\Core;
 
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use App\Doctrine\AbstractExtension;
 use App\Entity\Access\Access;
 use App\Entity\Core\AddressPostal;
 use Doctrine\ORM\QueryBuilder;
@@ -15,22 +13,12 @@ use Symfony\Component\Security\Core\Security;
  * Class AllowedAddressPostalExtension : Filtre de sécurité par défaut pour une resource AddressPostal
  * @package App\Doctrine\Core
  */
-final class AllowedAddressPostalExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class AllowedAddressPostalExtension extends AbstractExtension
 {
     public function __construct(private Security $security)
     { }
 
-    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
     {
         if (AddressPostal::class !== $resourceClass) {
             return;

+ 3 - 15
src/Doctrine/Core/CurrentNotificationUserExtension.php

@@ -3,9 +3,7 @@ declare(strict_types=1);
 
 namespace App\Doctrine\Core;
 
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use App\Doctrine\AbstractExtension;
 use App\Entity\Access\Access;
 use App\Entity\Core\NotificationUser;
 use Doctrine\ORM\QueryBuilder;
@@ -15,25 +13,15 @@ use Symfony\Component\Security\Core\Security;
  * Class NotificationExtension : Filtre de sécurité par défaut pour une resource Notification
  * @package App\Doctrine\Core
  */
-final class CurrentNotificationUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentNotificationUserExtension extends AbstractExtension
 {
     public function __construct(private Security $security)
     { }
 
-    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
     /**
      * @todo : A la suite de la migration, il faut supprimer le where avec le discr.
      */
-    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
     {
         if (NotificationUser::class !== $resourceClass) {
             return;

+ 3 - 15
src/Doctrine/Core/CurrentUserNotificationExtension.php

@@ -3,9 +3,7 @@ declare(strict_types=1);
 
 namespace App\Doctrine\Core;
 
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use App\Doctrine\AbstractExtension;
 use App\Entity\Access\Access;
 use App\Entity\Core\Notification;
 use Doctrine\ORM\QueryBuilder;
@@ -15,25 +13,15 @@ use Symfony\Component\Security\Core\Security;
  * Class NotificationExtension : Filtre de sécurité par défaut pour une resource Notification
  * @package App\Doctrine\Core
  */
-final class CurrentUserNotificationExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentUserNotificationExtension extends AbstractExtension
 {
     public function __construct(private Security $security)
     { }
 
-    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
     /**
      * @todo : A la suite de la migration, il faut supprimer le where avec le discr.
      */
-    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
     {
         if (Notification::class !== $resourceClass) {
             return;

+ 35 - 0
src/Doctrine/Education/CurrentCycleExtension.php

@@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Doctrine\Education;
+
+use App\Doctrine\AbstractExtension;
+use App\Entity\Access\Access;
+use App\Entity\Education\Cycle;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\Security\Core\Security;
+
+/**
+ * Class CurrentCycleExtension : Filtre de sécurité par défaut pour une resource Cycle
+ * @package App\Doctrine\Core
+ */
+final class CurrentCycleExtension extends AbstractExtension
+{
+    public function __construct(private Security $security)
+    { }
+
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    {
+        if (Cycle::class !== $resourceClass) {
+            return;
+        }
+
+        /** @var Access $currentUser */
+        $currentUser = $this->security->getUser();
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder
+            ->andWhere(sprintf('%s.organization = :organization', $rootAlias))
+            ->setParameter('organization', $currentUser->getOrganization())
+        ;
+    }
+}

+ 35 - 0
src/Doctrine/Education/CurrentEducationNotationConfigExtension.php

@@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Doctrine\Education;
+
+use App\Doctrine\AbstractExtension;
+use App\Entity\Access\Access;
+use App\Entity\Education\EducationNotationConfig;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\Security\Core\Security;
+
+/**
+ * Class CurrentEducationNotationConfigExtension : Filtre de sécurité par défaut pour une resource EducationNotationConfig
+ * @package App\Doctrine\Core
+ */
+final class CurrentEducationNotationConfigExtension extends AbstractExtension
+{
+    public function __construct(private Security $security)
+    { }
+
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    {
+        if (EducationNotationConfig::class !== $resourceClass) {
+            return;
+        }
+
+        /** @var Access $currentUser */
+        $currentUser = $this->security->getUser();
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder
+            ->andWhere(sprintf('%s.organization = :organization', $rootAlias))
+            ->setParameter('organization', $currentUser->getOrganization())
+        ;
+    }
+}

+ 35 - 0
src/Doctrine/Education/CurrentEducationTimingExtension.php

@@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Doctrine\Education;
+
+use App\Doctrine\AbstractExtension;
+use App\Entity\Access\Access;
+use App\Entity\Education\EducationTiming;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\Security\Core\Security;
+
+/**
+ * Class CurrentEducationTimingExtension : Filtre de sécurité par défaut pour une resource CurrentEducationTiming
+ * @package App\Doctrine\Core
+ */
+final class CurrentEducationTimingExtension extends AbstractExtension
+{
+    public function __construct(private Security $security)
+    { }
+
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    {
+        if (EducationTiming::class !== $resourceClass) {
+            return;
+        }
+
+        /** @var Access $currentUser */
+        $currentUser = $this->security->getUser();
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder
+            ->andWhere(sprintf('%s.organization = :organization', $rootAlias))
+            ->setParameter('organization', $currentUser->getOrganization())
+        ;
+    }
+}

+ 3 - 15
src/Doctrine/Network/CurrentNetworkOrganizationExtension.php

@@ -3,9 +3,7 @@ declare(strict_types=1);
 
 namespace App\Doctrine\Network;
 
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use App\Doctrine\AbstractExtension;
 use App\Entity\Access\Access;
 use App\Entity\Network\NetworkOrganization;
 use Doctrine\ORM\QueryBuilder;
@@ -15,22 +13,12 @@ use Symfony\Component\Security\Core\Security;
  * Class CurrentNetworkOrganizationExtension : Filtre de sécurité par défaut pour une resource NetworkOrganization
  * @package App\Doctrine\Core
  */
-final class CurrentNetworkOrganizationExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentNetworkOrganizationExtension extends AbstractExtension
 {
     public function __construct(private Security $security)
     { }
 
-    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
     {
         if (NetworkOrganization::class !== $resourceClass) {
             return;

+ 3 - 15
src/Doctrine/Organization/CurrentOrganizationAddressPostalExtension.php

@@ -3,9 +3,7 @@ declare(strict_types=1);
 
 namespace App\Doctrine\Organization;
 
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use App\Doctrine\AbstractExtension;
 use App\Entity\Access\Access;
 use App\Entity\Organization\OrganizationAddressPostal;
 use Doctrine\ORM\QueryBuilder;
@@ -15,22 +13,12 @@ use Symfony\Component\Security\Core\Security;
  * Class OrganizationAddressPosteExtension : Filtre de sécurité par défaut pour une resource OrganizationAddressPostal
  * @package App\Doctrine\Core
  */
-final class CurrentOrganizationAddressPostalExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentOrganizationAddressPostalExtension extends AbstractExtension
 {
     public function __construct(private Security $security)
     { }
 
-    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
     {
         if (OrganizationAddressPostal::class !== $resourceClass) {
             return;

+ 3 - 15
src/Doctrine/Organization/CurrentOrganizationArticleExtension.php

@@ -3,9 +3,7 @@ declare(strict_types=1);
 
 namespace App\Doctrine\Organization;
 
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use App\Doctrine\AbstractExtension;
 use App\Entity\Access\Access;
 use App\Entity\Organization\OrganizationArticle;
 use Doctrine\ORM\QueryBuilder;
@@ -15,22 +13,12 @@ use Symfony\Component\Security\Core\Security;
  * Class CurrentOrganizationArticleExtension : Filtre de sécurité par défaut pour une resource OrganizationArticle
  * @package App\Doctrine\Core
  */
-final class CurrentOrganizationArticleExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentOrganizationArticleExtension extends AbstractExtension
 {
     public function __construct(private Security $security)
     { }
 
-    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
     {
         if (OrganizationArticle::class !== $resourceClass) {
             return;

+ 3 - 15
src/Doctrine/Organization/CurrentOrganizationExtension.php

@@ -3,9 +3,7 @@ declare(strict_types=1);
 
 namespace App\Doctrine\Organization;
 
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
-use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use App\Doctrine\AbstractExtension;
 use App\Entity\Access\Access;
 use App\Entity\Organization\Organization;
 use Doctrine\ORM\QueryBuilder;
@@ -15,22 +13,12 @@ use Symfony\Component\Security\Core\Security;
  * Class CurrentOrganizationExtension : Filtre de sécurité par défaut pour une resource Organization
  * @package App\Doctrine\Core
  */
-final class CurrentOrganizationExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentOrganizationExtension extends AbstractExtension
 {
     public function __construct(private Security $security)
     { }
 
-    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []): void
-    {
-        $this->addWhere($queryBuilder, $resourceClass, $operationName);
-    }
-
-    private function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
+    protected function addWhere(QueryBuilder $queryBuilder, string $resourceClass, string $operationName): void
     {
         if (Organization::class !== $resourceClass) {
             return;

+ 35 - 1
src/Entity/Access/Access.php

@@ -4,6 +4,8 @@ declare(strict_types=1);
 namespace App\Entity\Access;
 
 use ApiPlatform\Core\Annotation\ApiFilter;
+use App\Entity\Education\EducationNotationConfig;
+use App\Entity\Organization\Parameters;
 use App\Filter\Person\FullNameFilter;
 use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\BooleanFilter;
 use ApiPlatform\Core\Annotation\ApiResource;
@@ -14,6 +16,7 @@ use App\Entity\Core\Notification;
 use App\Entity\Core\NotificationUser;
 use App\Entity\Organization\Organization;
 use App\Entity\Organization\OrganizationLicence;
+use App\Filter\Utils\InFilter;
 use App\Repository\Access\AccessRepository;
 use App\Entity\Person\Person;
 use App\Entity\Person\PersonActivity;
@@ -31,6 +34,7 @@ use Symfony\Component\Serializer\Annotation\Groups;
 #[ORM\Entity(repositoryClass: AccessRepository::class)]
 #[ApiFilter(BooleanFilter::class, properties: ['person.isPhysical'])]
 #[ApiFilter(FullNameFilter::class)]
+#[ApiFilter(InFilter::class, properties: ['id'])]
 class Access implements UserInterface
 {
     #[ORM\Id]
@@ -55,7 +59,7 @@ class Access implements UserInterface
     #[Groups(["access_people_ref", "access_address"])]
     private Person $person;
 
-    #[ORM\ManyToOne]
+    #[ORM\ManyToOne(inversedBy: 'accesses')]
     #[ORM\JoinColumn(nullable: false)]
     private Organization $organization;
 
@@ -107,6 +111,12 @@ class Access implements UserInterface
     #[ORM\OneToMany(mappedBy: 'access', targetEntity: AccessIntangible::class, cascade: ['persist'], orphanRemoval: true)]
     private Collection $accessIntangibles;
 
+    #[ORM\ManyToOne(inversedBy: 'publicationDirectors')]
+    private ?Parameters $publicationDirector;
+
+    #[ORM\ManyToOne(inversedBy: 'teachers')]
+    private ?EducationNotationConfig $educationNotationConfig;
+
     #[Pure] public function __construct()
     {
         $this->personActivity = new ArrayCollection();
@@ -500,6 +510,30 @@ class Access implements UserInterface
         return $this;
     }
 
+    public function getPublicationDirector(): ?Parameters
+    {
+        return $this->publicationDirector;
+    }
+
+    public function setPublicationDirector(?Parameters $parameters): self
+    {
+        $this->publicationDirector = $parameters;
+
+        return $this;
+    }
+
+    public function getEducationNotationConfig(): ?EducationNotationConfig
+    {
+        return $this->educationNotationConfig;
+    }
+
+    public function setEducationNotationConfig(?EducationNotationConfig $educationNotationConfig): self
+    {
+        $this->educationNotationConfig = $educationNotationConfig;
+
+        return $this;
+    }
+
     #[Pure] public function getUserIdentifier(): string
     {
         return $this->person->getUsername();

+ 39 - 0
src/Entity/Billing/BillingSetting.php

@@ -4,9 +4,13 @@ declare(strict_types=1);
 namespace App\Entity\Billing;
 
 use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Annotation\ApiSubresource;
 use App\Entity\Organization\Organization;
 use App\Repository\Billing\BillingSettingRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
+use JetBrains\PhpStorm\Pure;
 
 #[ApiResource(
     collectionOperations: [],
@@ -24,9 +28,17 @@ class BillingSetting
     #[ORM\JoinColumn(nullable: false)]
     private Organization $organization;
 
+    #[ORM\OneToMany( mappedBy: 'billingSetting', targetEntity: ResidenceArea::class, cascade: ['persist'], orphanRemoval: true)]
+    private Collection $residenceAreas;
+
     #[ORM\Column(options: ['default' => false])]
     private bool $applyVat = false;
 
+    #[Pure] public function __construct()
+    {
+        $this->residenceAreas = new ArrayCollection();
+    }
+
     public function getId(): ?int
     {
         return $this->id;
@@ -56,4 +68,31 @@ class BillingSetting
 
         return $this;
     }
+
+    public function getResidenceAreas(): Collection
+    {
+        return $this->residenceAreas;
+    }
+
+    public function addResidenceArea(ResidenceArea $residenceArea): self
+    {
+        if (!$this->residenceAreas->contains($residenceArea)) {
+            $this->residenceAreas[] = $residenceArea;
+            $residenceArea->setBillingSetting($this);
+        }
+
+        return $this;
+    }
+
+    public function removeResidenceArea(ResidenceArea $residenceArea): self
+    {
+        if ($this->residenceAreas->removeElement($residenceArea)) {
+            // set the owning side to null (unless already changed)
+            if ($residenceArea->getBillingSetting() === $this) {
+                $residenceArea->setBillingSetting(null);
+            }
+        }
+
+        return $this;
+    }
 }

+ 66 - 0
src/Entity/Billing/ResidenceArea.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Entity\Billing;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use App\Annotation\BillingSettingDefaultValue;
+use App\Repository\Billing\ResidenceAreaRepository;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * Zone de résidence d'un Access, telle que définie par l'Organization
+ */
+#[ApiResource(
+    collectionOperations: [
+        "get" => ["security" => "is_granted('ROLE_ORGANIZATION_VIEW')"],
+        "post"
+    ],
+    itemOperations: [
+        "get" => ["security" => "is_granted('ROLE_ORGANIZATION_VIEW') and object.getBillingSetting().getOrganization().getId() == user.getOrganization().getId()"],
+        "put" => ["security" => "object.getBillingSetting().getOrganization().getId() == user.getOrganization().getId()"],
+        "delete" => ["security" => "object.getBillingSetting().getOrganization().getId() == user.getOrganization().getId()"],
+    ],
+    attributes: ["security" => "is_granted('ROLE_ORGANIZATION')"]
+)]
+#[BillingSettingDefaultValue(fieldName: "billingSetting")]
+#[ORM\Entity(repositoryClass: ResidenceAreaRepository::class)]
+class ResidenceArea
+{
+    #[ORM\Id]
+    #[ORM\Column]
+    #[ORM\GeneratedValue]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'residenceAreas')]
+    private BillingSetting $billingSetting;
+
+    #[ORM\Column(length: 255)]
+    private string $label;
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getLabel(): string
+    {
+        return $this->label;
+    }
+
+    public function setLabel(string $label){
+        $this->label = $label;
+        return $this;
+    }
+
+    public function getBillingSetting(): BillingSetting
+    {
+        return $this->billingSetting;
+    }
+
+    public function setBillingSetting(BillingSetting $billingSetting): self
+    {
+        $this->billingSetting = $billingSetting;
+
+        return $this;
+    }
+}

+ 88 - 0
src/Entity/Booking/Course.php

@@ -0,0 +1,88 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Entity\Booking;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use App\Annotation\OrganizationDefaultValue;
+use App\Annotation\ActivityYearConstraintAware;
+use App\Entity\Organization\Organization;
+use App\Entity\Traits\ActivityYearTrait;
+use App\Repository\Booking\CourseRepository;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * @todo : A la suite de la migration, il faut supprimer le nom de la table pour avoir une table Course, et supprimer l'attribut discr.
+ *
+ * Classe Course qui permet de gérer les cours de la structure.
+ */
+#[ApiResource(
+    collectionOperations:[
+        'get'
+    ],
+    itemOperations: [
+        "get" => ["security" => "is_granted('ROLE_COURSE_VIEW') and object.getOrganization().getId() == user.getOrganization().getId()"],
+    ]
+)]
+#[ORM\Entity(repositoryClass:CourseRepository::class)]
+#[ActivityYearConstraintAware(startYearFieldName: "startYear", endYearFieldName: "endYear")]
+#[OrganizationDefaultValue(fieldName: "organization")]
+
+#[ORM\Table(name: 'Booking')]
+class Course
+{
+    use ActivityYearTrait;
+
+    #[ORM\Id]
+    #[ORM\Column]
+    #[ORM\GeneratedValue]
+    private ?int $id = null;
+
+    #[ORM\Column(length: 255, nullable: false)]
+    private string $discr = 'course';
+
+    #[ORM\ManyToOne(inversedBy: 'organizationAddressPostals')]
+    #[ORM\JoinColumn(nullable: false)]
+    private Organization $organization;
+
+    #[ORM\Column(type: 'datetime', nullable: true)]
+    private ?\DateTimeInterface $datetimeStart = null;
+
+    #[ORM\Column(type: 'datetime', nullable: true)]
+    private ?\DateTimeInterface $datetimeEnd = null;
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getOrganization(): ?Organization
+    {
+        return $this->organization;
+    }
+
+    public function setOrganization(Organization $organization): self
+    {
+        $this->organization = $organization;
+
+        return $this;
+    }
+
+    public function getDatetimeStart(): ?\DateTimeInterface {
+        return $this->datetimeStart;
+    }
+
+    public function setDatetimeStart(?\DateTimeInterface $datetimeStart = null): self {
+        $this->datetimeStart = $datetimeStart;
+        return $this;
+    }
+
+    public function getDatetimeEnd(): ?\DateTimeInterface {
+        return $this->datetimeEnd;
+    }
+
+    public function setDatetimeEnd(?\DateTimeInterface $datetimeEnd = null) :self {
+        $this->datetimeEnd = $datetimeEnd;
+        return $this;
+    }
+}

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

@@ -140,7 +140,7 @@ class ContactPoint
     {
         $this->faxNumber = $faxNumber;
 
-        if(!is_null($this->faxNumber && !is_null($this->getFaxNumberInvalid())))
+        if(!is_null($this->faxNumber) && !is_null($this->getFaxNumberInvalid()))
             $this->setFaxNumberInvalid(null);
 
         return $this;

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

@@ -7,6 +7,7 @@ use ApiPlatform\Core\Annotation\ApiResource;
 use App\Entity\Organization\Organization;
 use App\Entity\Person\Person;
 use App\Repository\Core\FileRepository;
+use App\Entity\Organization\Parameters;
 use DateTime;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
@@ -171,6 +172,9 @@ class File
     #[ORM\OneToMany(mappedBy: 'image', targetEntity: Organization::class, orphanRemoval: true)]
     private Collection $organizationImages;
 
+    #[ORM\OneToOne(mappedBy: "qrCode", targetEntity: Parameters::class, fetch: 'EAGER')]
+    private Parameters $qrCode;
+
     #[Pure] public function __construct()
     {
         $this->personImages = new ArrayCollection();
@@ -495,4 +499,15 @@ class File
 
         return $this;
     }
+
+    public function getQrCode(): Parameters
+    {
+        return $this->qrCode;
+    }
+
+    public function setQrCode(Parameters $qrCode): self
+    {
+        $this->qrCode = $qrCode;
+        return $this;
+    }
 }

+ 112 - 0
src/Entity/Education/Cycle.php

@@ -0,0 +1,112 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Entity\Education;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use App\Repository\Education\CycleRepository;
+use App\Entity\Organization\Organization;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * Enum des cycles éducatifs, utilisés par les EducationCurriculum
+ * NB: le nombre de cycles est fixé à 6, mais chaque Organization peut en modifier le label
+ */
+#[ApiResource(
+    collectionOperations: [
+        "get" => ["security" => "is_granted('ROLE_ORGANIZATION_VIEW')"]
+    ],
+    itemOperations: [
+        "get" => ["security" => "is_granted('ROLE_ORGANIZATION_VIEW') and object.getOrganization().getId() == user.getOrganization().getId()"],
+        "put" => ["security" => "object.getOrganization().getId() == user.getOrganization().getId()"],
+    ],
+    attributes: ["security" => "is_granted('ROLE_ORGANIZATION')"]
+)]
+#[ORM\Entity(repositoryClass: CycleRepository::class)]
+class Cycle
+{
+    #[ORM\Id]
+    #[ORM\Column]
+    #[ORM\GeneratedValue]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'cycles')]
+    private Organization $organization;
+
+    #[ORM\Column(nullable: true)]
+    private ?int $order = null;
+
+    #[ORM\Column(length: 255)]
+    private string $label;
+
+    #[ORM\Column(length: 255)]
+    #[Assert\Choice(callback: ['\App\Enum\Education\CycleEnum', 'toArray'], message: 'invalid-cycle')]
+    private string $cycleEnum;
+
+    #[ORM\Column(options: ['default' => false])]
+    private bool $isSystem = false;
+
+    public function getId() :?int
+    {
+        return $this->id;
+    }
+
+    public function setOrganization(Organization $organization): self
+    {
+        $this->organization = $organization;
+        return $this;
+    }
+
+    public function getOrganization(): Organization
+    {
+        return $this->organization;
+    }
+
+    public function setOrder(?int $order): self
+    {
+        $this->order = $order;
+        return $this;
+    }
+
+    public function getOrder(): ?int
+    {
+        return $this->order;
+    }
+
+
+    public function setLabel(string $label): self
+    {
+        $this->label = $label;
+        return $this;
+    }
+
+    public function getLabel(): string
+    {
+        return $this->label;
+    }
+
+    public function setCycleEnum(string $cycleEnum): self
+    {
+        $this->cycleEnum = $cycleEnum;
+        return $this;
+    }
+
+    public function getCycleEnum(): string
+    {
+        return $this->cycleEnum;
+    }
+
+
+    public function setIsSystem(bool $isSystem): self
+    {
+        $this->isSystem = $isSystem;
+        return $this;
+    }
+
+
+    public function getIsSystem(): bool
+    {
+        return $this->isSystem;
+    }
+}

+ 44 - 0
src/Entity/Education/EducationCurriculum.php

@@ -0,0 +1,44 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Entity\Education;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use App\Repository\Education\EducationCurriculumRepository;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * Curriculum éducatif; composé d'un cycle, d'une année et d'un niveau
+ */
+#[ApiResource(
+    collectionOperations: [],
+    itemOperations: []
+)]
+#[ORM\Entity(repositoryClass: EducationCurriculumRepository::class)]
+class EducationCurriculum
+{
+    #[ORM\Id]
+    #[ORM\Column]
+    #[ORM\GeneratedValue]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'educationCurriculums')]
+    private ?EducationNotationConfig $educationNotationConfig;
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function setEducationNotationConfig(?EducationNotationConfig $educationNotationConfig): self
+    {
+        $this->educationNotationConfig = $educationNotationConfig;
+
+        return $this;
+    }
+
+    public function getEducationNotationConfig(): ?EducationNotationConfig
+    {
+        return $this->educationNotationConfig;
+    }
+}

+ 191 - 0
src/Entity/Education/EducationNotationConfig.php

@@ -0,0 +1,191 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Entity\Education;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use App\Annotation\OrganizationDefaultValue;
+use App\Entity\Access\Access;
+use App\Repository\Education\EducationNotationConfigRepository;
+use App\Entity\Organization\Organization;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ORM\Mapping as ORM;
+use JetBrains\PhpStorm\Pure;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * Configuration des grilles d'évaluation
+ */
+#[ApiResource(
+    collectionOperations: [
+        "get" => ["security" => "is_granted('ROLE_PEDAGOGICS_ADMINISTRATION_VIEW')"]
+    ],
+    itemOperations: [
+        "get" => ["security" => "is_granted('ROLE_PEDAGOGICS_ADMINISTRATION_VIEW') and object.getOrganization().getId() == user.getOrganization().getId()"],
+        "put" => ["security" => "object.getOrganization().getId() == user.getOrganization().getId()"],
+    ],
+    attributes: ["security" => "is_granted('ROLE_PEDAGOGICS_ADMINISTRATION')"]
+)]
+#[ORM\Entity(repositoryClass: EducationNotationConfigRepository::class)]
+#[OrganizationDefaultValue(fieldName: "organization")]
+class EducationNotationConfig
+{
+    #[ORM\Id]
+    #[ORM\Column]
+    #[ORM\GeneratedValue]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'educationNotationConfigs')]
+    private Organization $organization;
+
+    #[ORM\Column(length: 255)]
+    private string $label;
+
+    #[ORM\Column(options: ['default' => true])]
+    private bool $isActive = true;
+
+    #[ORM\Column(type: 'text', nullable: true)]
+    private ?string $description;
+
+    #[ORM\Column(options: ['default' => 1])]
+    #[Assert\Range(
+        notInRangeMessage: 'between_{{ min }}_and_{{ max }}',
+        min: 1,
+        max: 10
+    )]
+    private int $coefficient = 1;
+
+    #[ORM\OneToMany( mappedBy: 'educationNotationConfig', targetEntity: Access::class)]
+    private Collection $teachers;
+
+    #[ORM\OneToMany( mappedBy: 'educationNotationConfig', targetEntity: EducationCurriculum::class)]
+    private Collection $educationCurriculums;
+
+
+    #[Pure] public function __construct() {
+        $this->teachers = new ArrayCollection();
+        $this->educationCurriculums = new ArrayCollection();
+    }
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function setOrganization(Organization $organization): self
+    {
+        $this->organization = $organization;
+
+        return $this;
+    }
+
+    public function getOrganization(): Organization
+    {
+        return $this->organization;
+    }
+
+    public function setLabel(string $label): self
+    {
+        $this->label = $label;
+
+        return $this;
+    }
+
+    public function getLabel(): string
+    {
+        return $this->label;
+    }
+
+    public function setIsActive(bool $isActive): self
+    {
+        $this->isActive = $isActive;
+
+        return $this;
+    }
+
+    public function getIsActive(): bool
+    {
+        return $this->isActive;
+    }
+
+    public function setDescription(?string $description): self
+    {
+        $this->description = $description;
+
+        return $this;
+    }
+
+    public function getDescription(): ?string
+    {
+        return $this->description;
+    }
+
+    public function setCoefficient(?int $coefficient): self
+    {
+        if($coefficient === null) $coefficient = 1;
+        $this->coefficient = $coefficient;
+
+        return $this;
+    }
+
+    public function getCoefficient(): int
+    {
+        return $this->coefficient;
+    }
+
+
+    public function addTeacher(Access $teacher): self
+    {
+        if (!$this->teachers->contains($teacher)) {
+            $this->teachers[] = $teacher;
+            $teacher->setEducationNotationConfig($this);
+        }
+
+        return $this;
+    }
+
+    public function removeTeacher(Access $teacher): self
+    {
+        if ($this->teachers->removeElement($teacher)) {
+            // set the owning side to null (unless already changed)
+            if ($teacher->getEducationNotationConfig() === $this) {
+                $teacher->setEducationNotationConfig(null);
+            }
+        }
+        return $this;
+    }
+
+    public function getTeachers(): Collection
+    {
+        return $this->teachers;
+    }
+
+
+    public function addEducationCurriculum(EducationCurriculum $educationCurriculums): self
+    {
+        if (!$this->educationCurriculums->contains($educationCurriculums)) {
+            $this->educationCurriculums[] = $educationCurriculums;
+            $educationCurriculums->setEducationNotationConfig($this);
+        }
+
+        return $this;
+    }
+
+    public function removeEducationCurriculum(EducationCurriculum $educationCurriculums): self
+    {
+        if ($this->educationCurriculums->removeElement($educationCurriculums)) {
+            // set the owning side to null (unless already changed)
+            if ($educationCurriculums->getEducationNotationConfig() === $this) {
+                $educationCurriculums->setEducationNotationConfig(null);
+            }
+        }
+
+        return $this;
+    }
+
+    public function getEducationCurriculums(): Collection
+    {
+        return $this->educationCurriculums;
+    }
+}

+ 70 - 0
src/Entity/Education/EducationTiming.php

@@ -0,0 +1,70 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Entity\Education;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use App\Annotation\OrganizationDefaultValue;
+use App\Entity\Organization\Organization;
+use App\Repository\Education\EducationTimingRepository;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * Temps d'un enseignement
+ */
+#[ApiResource(
+    collectionOperations: [
+        "get" => ["security" => "is_granted('ROLE_ORGANIZATION_VIEW')"],
+        "post"
+    ],
+    itemOperations: [
+        "get" => ["security" => "is_granted('ROLE_ORGANIZATION_VIEW') and object.getOrganization().getId() == user.getOrganization().getId()"],
+        "put" => ["security" => "object.getOrganization().getId() == user.getOrganization().getId()"],
+        "delete" => ["security" => "object.getOrganization().getId() == user.getOrganization().getId()"],
+    ],
+    attributes: ["security" => "is_granted('ROLE_ORGANIZATION')"]
+)]
+#[OrganizationDefaultValue(fieldName: "organization")]
+#[ORM\Entity(repositoryClass: EducationTimingRepository::class)]
+class EducationTiming
+{
+    #[ORM\Id]
+    #[ORM\Column]
+    #[ORM\GeneratedValue]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'educationTimings')]
+    #[ORM\JoinColumn(nullable: false)]
+    private Organization $organization;
+
+    #[ORM\Column]
+    private int $timing;
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function setOrganization(Organization $organization): self
+    {
+        $this->organization = $organization;
+        return $this;
+    }
+
+    public function getOrganization(): Organization
+    {
+        return $this->organization;
+    }
+
+    public function setTiming(int $timing): self
+    {
+        $this->timing = $timing;
+
+        return $this;
+    }
+
+    public function getTiming(): int
+    {
+        return $this->timing;
+    }
+}

+ 109 - 2
src/Entity/Organization/Organization.php

@@ -5,10 +5,14 @@ namespace App\Entity\Organization;
 
 use ApiPlatform\Core\Annotation\ApiResource;
 use ApiPlatform\Core\Annotation\ApiSubresource;
+use App\Entity\Access\Access;
 use App\Entity\Billing\BillingSetting;
 use App\Entity\Core\BankAccount;
 use App\Entity\Core\ContactPoint;
 use App\Entity\Core\File;
+use App\Entity\Education\Cycle;
+use App\Entity\Education\EducationNotationConfig;
+use App\Entity\Education\EducationTiming;
 use App\Entity\Network\NetworkOrganization;
 use App\Repository\Organization\OrganizationRepository;
 use Doctrine\Common\Collections\ArrayCollection;
@@ -55,6 +59,9 @@ class Organization
     #[ORM\OneToOne(mappedBy: 'organization', cascade: ['persist', 'remove'])]
     private Settings $settings;
 
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Access::class, orphanRemoval: true)]
+    private Collection $accesses;
+
     #[ORM\OneToMany(mappedBy: 'organization', targetEntity: NetworkOrganization::class, orphanRemoval: true)]
     #[ApiSubresource]
     private Collection $networkOrganizations;
@@ -62,14 +69,18 @@ class Organization
     #[ORM\OneToMany(mappedBy: 'parent', targetEntity: NetworkOrganization::class, orphanRemoval: true)]
     private Collection $networkOrganizationChildren;
 
-    #[ORM\OneToOne(cascade: ['persist', 'remove'])]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: EducationNotationConfig::class, orphanRemoval: true)]
+    private Collection $educationNotationConfigs;
+
+    #[ORM\OneToOne( inversedBy: 'organization', targetEntity: Parameters::class)]
     #[ORM\JoinColumn(nullable: false)]
     private Parameters $parameters;
 
     #[ORM\OneToOne(mappedBy: 'organization', cascade: ['persist', 'remove'], orphanRemoval: true)]
+    #[ApiSubresource]
     private BillingSetting $billingSetting;
 
-    #[ORM\Column(length: 255, nullable: true)]
+    #[ORM\Column(type: 'text', nullable: true)]
     private ?string $description = null;
 
     #[ORM\Column(type: 'date', nullable: true)]
@@ -207,8 +218,15 @@ class Organization
     #[ApiSubresource]
     private Collection $organizationArticles;
 
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Cycle::class, orphanRemoval: true)]
+    private Collection $cycles;
+
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: EducationTiming::class, orphanRemoval: true)]
+    private Collection $educationTimings;
+
     #[Pure] public function __construct()
     {
+        $this->accesses = new ArrayCollection();
         $this->networkOrganizations = new ArrayCollection();
         $this->networkOrganizationChildren = new ArrayCollection();
         $this->typeOfPractices = new ArrayCollection();
@@ -217,6 +235,9 @@ class Organization
         $this->organizationAddressPostals = new ArrayCollection();
         $this->organizationLicences = new ArrayCollection();
         $this->organizationArticles = new ArrayCollection();
+        $this->cycles = new ArrayCollection();
+        $this->educationTimings = new ArrayCollection();
+        $this->educationNotationConfigs = new ArrayCollection();
     }
 
     public function getId(): ?int
@@ -289,6 +310,11 @@ class Organization
         return $this;
     }
 
+    public function getAccesses(): Collection
+    {
+        return $this->accesses;
+    }
+
     public function getNetworkOrganizations(): Collection
     {
         return $this->networkOrganizations;
@@ -954,4 +980,85 @@ class Organization
 
         return $this;
     }
+
+    public function addCycle(Cycle $cycle): self
+    {
+        if (!$this->cycles->contains($cycle)) {
+            $this->cycles[] = $cycle;
+            $cycle->setOrganization($this);
+        }
+
+        return $this;
+    }
+
+    public function removeCycle(Cycle $cycle): self
+    {
+        if ($this->cycles->removeElement($cycle)) {
+            // set the owning side to null (unless already changed)
+            if ($cycle->getOrganization() === $this) {
+                $cycle->setOrganization(null);
+            }
+        }
+
+        return $this;
+    }
+
+    public function getCycles(): Collection
+    {
+        return $this->cycles;
+    }
+
+    public function addEducationNotationConfig(EducationNotationConfig $educationNotationConfig): self
+    {
+        if (!$this->educationNotationConfigs->contains($educationNotationConfig)) {
+            $this->educationNotationConfigs[] = $educationNotationConfig;
+            $educationNotationConfig->setOrganization($this);
+        }
+
+        return $this;
+    }
+
+    public function removeEducationNotationConfig(EducationNotationConfig $educationNotationConfig): self
+    {
+        if ($this->educationNotationConfigs->removeElement($educationNotationConfig)) {
+            // set the owning side to null (unless already changed)
+            if ($educationNotationConfig->getOrganization() === $this) {
+                $educationNotationConfig->setOrganization(null);
+            }
+        }
+
+        return $this;
+    }
+
+    public function getEducationNotationConfigs(): Collection
+    {
+        return $this->educationNotationConfigs;
+    }
+
+    public function addEducationTiming(EducationTiming $educationTiming): self
+    {
+        if (!$this->educationTimings->contains($educationTiming)) {
+            $this->educationTimings[] = $educationTiming;
+            $educationTiming->setOrganization($this);
+        }
+
+        return $this;
+    }
+
+    public function removeEducationTiming(EducationTiming $educationTiming): self
+    {
+        if ($this->educationTimings->removeElement($educationTiming)) {
+            // set the owning side to null (unless already changed)
+            if ($educationTiming->getOrganization() === $this) {
+                $educationTiming->setOrganization(null);
+            }
+        }
+
+        return $this;
+    }
+
+    public function getEducationTimings(): Collection
+    {
+        return $this->educationTimings;
+    }
 }

+ 152 - 24
src/Entity/Organization/Parameters.php

@@ -4,16 +4,30 @@ declare(strict_types=1);
 namespace App\Entity\Organization;
 
 use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Annotation\ApiSubresource;
+use App\Entity\Access\Access;
 use App\Entity\Core\File;
 use App\Repository\Organization\ParametersRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
+use JetBrains\PhpStorm\Pure;
 use Symfony\Component\Validator\Constraints as Assert;
+use App\Validator\Organization\Parameters as OpentalentAssert;
 
 #[ApiResource(
     collectionOperations: [],
-    itemOperations: ['get']
+    itemOperations: [
+        'get' => [
+            'security' => '(is_granted("ROLE_ORGANIZATION_VIEW") or is_granted("ROLE_ORGANIZATION")) and object.getOrganization().getId() == user.getOrganization().getId()'
+        ],
+        'put' => [
+            'security' => 'is_granted("ROLE_ORGANIZATION") and object.getOrganization().getId() == user.getOrganization().getId()'
+        ]
+    ]
 )]
 #[ORM\Entity(repositoryClass: ParametersRepository::class)]
+#[OpentalentAssert\MobytCredentials]
 class Parameters
 {
     #[ORM\Id]
@@ -21,6 +35,9 @@ class Parameters
     #[ORM\GeneratedValue]
     private ?int $id = null;
 
+    #[ORM\OneToOne(mappedBy: 'parameters', targetEntity: Organization::class)]
+    private Organization $organization;
+
     #[ORM\Column(type: 'date', nullable: true)]
     private ?\DateTimeInterface $financialDate = null;
 
@@ -36,29 +53,42 @@ class Parameters
     #[ORM\Column(options: ['default' => false])]
     private bool $trackingValidation = false;
 
+    #[ORM\Column(options: ['default' => 20])]
+    #[Assert\Range(
+        notInRangeMessage: 'between_{{ min }}_and_{{ max }}',
+        min: 0,
+        max: 100
+    )]
+    private int $average = 20;
+
     #[ORM\Column(options: ['default' => true])]
     private bool $editCriteriaNotationByAdminOnly = true;
 
     #[ORM\Column(length: 255, nullable: true)]
+    #[Assert\Regex('/^[a-z0-9]+$/i', message: 'smsSenderName_error')]
     private ?string $smsSenderName = null;
 
     #[ORM\Column(options: ['default' => false])]
     private bool $logoDonorsMove = false;
 
-    #[ORM\Column(length: 255, nullable: true)]
+    #[ORM\Column(length: 60, nullable: true)]
     private ?string $subDomain = null;
 
-    #[ORM\Column(length: 255, nullable: true)]
+    #[ORM\Column(length: 100, nullable: true)]
     private ?string $website = null;
 
-    #[ORM\Column(length: 255, nullable: true)]
+    #[ORM\Column(length: 150, nullable: true)]
     private ?string $otherWebsite = null;
 
     #[ORM\Column(options: ['default' => false])]
     private bool $desactivateOpentalentSiteWeb = false;
 
+    #[ORM\OneToMany( mappedBy: 'publicationDirector', targetEntity: Access::class)]
+    #[ApiSubresource]
+    private Collection $publicationDirectors;
+
     #[ORM\Column(length: 255, nullable: true)]
-    #[Assert\Choice(callback: ['\App\Enum\Organization\OpcBulletinPeriodEnumaEnum', 'toArray'], message: 'invalid-bulletin-period')]
+    #[Assert\Choice(callback: ['\App\Enum\Organization\BulletinPeriodEnum', 'toArray'], message: 'invalid-bulletin-period')]
     private ?string $bulletinPeriod = null;
 
     #[ORM\Column(options: ['default' => false])]
@@ -89,12 +119,6 @@ class Parameters
     #[Assert\Choice(callback: ['\App\Enum\Organization\BulletinOutputEnum', 'toArray'], message: 'invalid-bulletin-output')]
     private ?string $bulletinOutput = null;
 
-    #[ORM\Column(length: 255, nullable: true)]
-    private ?string $usernameSMS = null;
-
-    #[ORM\Column(length: 255, nullable: true)]
-    private ?string $passwordSMS = null;
-
     #[ORM\Column(options: ['default' => true])]
     private bool $bulletinEditWithoutEvaluation = true;
 
@@ -102,28 +126,62 @@ class Parameters
     #[Assert\Choice(callback: ['\App\Enum\Organization\SendToBulletinEnum', 'toArray'], message: 'invalid-send-to-bulletin')]
     private ?string $bulletinReceiver = null;
 
+    #[ORM\Column(length: 255, nullable: true)]
+    private ?string $usernameSMS = null;
+
+    #[ORM\Column(length: 255, nullable: true)]
+    private ?string $passwordSMS = null;
+
     #[ORM\Column(options: ['default' => true])]
     private bool $showAdherentList = true;
 
     #[ORM\Column(options: ['default' => false])]
     private bool $studentsAreAdherents = false;
 
+    #[ORM\OneToOne(inversedBy: 'qrCode', targetEntity: File::class, cascade: ['persist'], fetch: 'EAGER')]
+    #[ORM\JoinColumn(referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
+    private ?File $qrCode = null;
+
     #[ORM\Column(length: 255, options: ['default' => 'Europe/Paris'])]
-    #[Assert\Choice(callback: ['\App\Enum\Organization\TimeZoneEnum', 'toArray'], message: 'invalid-timezone')]
+    #[Assert\Choice(callback: ['\App\Enum\Core\TimeZoneEnum', 'toArray'], message: 'invalid-timezone')]
     private ?string $timezone = "Europe/Paris";
 
-    #[ORM\Column(length: 255, nullable: true)]
-    #[Assert\Choice(callback: ['\App\Enum\Organization\PeriodicityEnum', 'toArray'], message: 'invalid-periodicity')]
+    #[ORM\Column(length: 255, nullable: true, options: ['default' => 'ANNUAL'])]
+    #[Assert\Choice(callback: ['\App\Enum\Education\PeriodicityEnum', 'toArray'], message: 'invalid-periodicity')]
     private ?string $educationPeriodicity = null;
 
-    #[ORM\OneToOne(cascade: ['persist', 'remove'])]
-    private File $qrCode;
+    #[ORM\Column(length: 255, nullable: true, options: ['default' => 'BY_EDUCATION'])]
+    #[Assert\Choice(callback: ['\App\Enum\Education\AdvancedEducationNotationTypeEnum', 'toArray'], message: 'invalid-advanced-education-notation-type')]
+    private ?string $advancedEducationNotationType = null;
+
+    #[ORM\Column(options: ['default' => false])]
+    private bool $sendAttendanceEmail = false;
+
+    #[ORM\Column(options: ['default' => false])]
+    private bool $sendAttendanceSms = false;
+
+    #[Pure] public function __construct()
+    {
+        $this->publicationDirectors = new ArrayCollection();
+    }
 
     public function getId(): ?int
     {
         return $this->id;
     }
 
+    public function getOrganization(): Organization
+    {
+        return $this->organization;
+    }
+
+    public function setOrganization(Organization $organization): self
+    {
+        $this->organization = $organization;
+
+        return $this;
+    }
+
     public function getFinancialDate(): ?\DateTimeInterface
     {
         return $this->financialDate;
@@ -184,6 +242,18 @@ class Parameters
         return $this;
     }
 
+    public function getAverage(): int
+    {
+        return $this->average;
+    }
+
+    public function setAverage(int $average)
+    {
+        $this->average = $average;
+
+        return $this;
+    }
+
     public function getEditCriteriaNotationByAdminOnly(): bool
     {
         return $this->editCriteriaNotationByAdminOnly;
@@ -280,6 +350,33 @@ class Parameters
         return $this;
     }
 
+    public function getPublicationDirectors(): Collection
+    {
+        return $this->publicationDirectors;
+    }
+
+    public function addPublicationDirector(Access $access): self
+    {
+        if (!$this->publicationDirectors->contains($access)) {
+            $this->publicationDirectors[] = $access;
+            $access->setPublicationDirector($this);
+        }
+
+        return $this;
+    }
+
+    public function removePublicationDirector(Access $access): self
+    {
+        if ($this->publicationDirectors->removeElement($access)) {
+            // set the owning side to null (unless already changed)
+            if ($access->getPublicationDirector() === $this) {
+                $access->setPublicationDirector(null);
+            }
+        }
+
+        return $this;
+    }
+
     public function getBulletinWithTeacher(): bool
     {
         return $this->bulletinWithTeacher;
@@ -484,19 +581,50 @@ class Parameters
         return $this;
     }
 
-    /**
-     * @return File
-     */
-    public function getQrCode(): File
+    public function getAdvancedEducationNotationType(): ?string
+    {
+        return $this->advancedEducationNotationType;
+    }
+
+    public function setAdvancedEducationNotationType(?string $advancedEducationNotationType): self
+    {
+        $this->advancedEducationNotationType = $advancedEducationNotationType;
+
+        return $this;
+    }
+
+    public function getQrCode(): ?File
     {
         return $this->qrCode;
     }
 
-    /**
-     * @param File $qrCode
-     */
-    public function setQrCode(File $qrCode): void
+    public function setQrCode(?File $qrCode): self
     {
         $this->qrCode = $qrCode;
+        return $this;
+    }
+
+    public function getSendAttendanceSms(): bool
+    {
+        return $this->sendAttendanceSms;
+    }
+
+    public function setSendAttendanceSms(bool $sendAttendanceSms): self
+    {
+        $this->sendAttendanceSms = $sendAttendanceSms;
+
+        return $this;
+    }
+
+    public function getSendAttendanceEmail(): bool
+    {
+        return $this->sendAttendanceEmail;
+    }
+
+    public function setSendAttendanceEmail(bool $sendAttendanceEmail): self
+    {
+        $this->sendAttendanceEmail = $sendAttendanceEmail;
+
+        return $this;
     }
 }

+ 15 - 0
src/Enum/Education/AdvancedEducationNotationTypeEnum.php

@@ -0,0 +1,15 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Enum\Education;
+
+use MyCLabs\Enum\Enum;
+
+/**
+ * Type possible sur lesquels les grilles péda doivent se baser
+ */
+class AdvancedEducationNotationTypeEnum extends Enum
+{
+    private const BY_TEACHER ='BY_TEACHER';
+    private const BY_EDUCATION ='BY_EDUCATION';
+}

+ 19 - 0
src/Enum/Education/CycleEnum.php

@@ -0,0 +1,19 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Enum\Education;
+
+use MyCLabs\Enum\Enum;
+
+/**
+ * Cycle Enum.
+ */
+class CycleEnum extends Enum
+{
+    private const INITIATION_CYCLE = 'INITIATION_CYCLE';
+    private const CYCLE_1 = 'CYCLE_1';
+    private const CYCLE_2 = 'CYCLE_2';
+    private const CYCLE_3 = 'CYCLE_3';
+    private const CYCLE_4 = 'CYCLE_4';
+    private const OUT_CYCLE = 'OUT_CYCLE';
+}

+ 11 - 5
src/EventListener/DoctrineFilter/DoctrineFilterListener.php

@@ -3,7 +3,8 @@ declare(strict_types=1);
 
 namespace App\EventListener\DoctrineFilter;
 
-use App\Service\Utils\DateTimeConstraint;
+use App\Service\Constraint\ActivityYearConstraint;
+use App\Service\Constraint\DateTimeConstraint;
 use Doctrine\ORM\EntityManagerInterface;
 use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\HttpKernel\Event\RequestEvent;
@@ -17,6 +18,7 @@ class DoctrineFilterListener
     public function __construct(
         private EntityManagerInterface $entityManager,
         private DateTimeConstraint $dateTimeConstraint,
+        private ActivityYearConstraint $activityYearConstraint,
         private Security $security,
         private RequestStack $requestStack
     )
@@ -29,10 +31,14 @@ class DoctrineFilterListener
             // don't do anything if it's not the main request
             return;
         }
-        $filter = $this->entityManager->getFilters()->getFilter('date_time_filter');
-        $filter->setParameter('accessId', $this->security->getUser()?->getId() ?? null);
-        $filter->setParameter('_time_constraint', $this->requestStack->getMainRequest()->get('_time_constraint', true));
+        $dateTimeFilter = $this->entityManager->getFilters()->getFilter('date_time_filter');
+        $dateTimeFilter->setParameter('accessId', $this->security->getUser()?->getId() ?? null);
+        $dateTimeFilter->setParameter('_time_constraint', $this->requestStack->getMainRequest()->get('_time_constraint', true));
+        $dateTimeFilter->setDateTimeConstraint($this->dateTimeConstraint);
 
-        $filter->setDateTimeConstraint($this->dateTimeConstraint);
+        $activityYearFilter = $this->entityManager->getFilters()->getFilter('activity_year_filter');
+        $activityYearFilter->setParameter('accessId', $this->security->getUser()?->getId() ?? null);
+        $activityYearFilter->setParameter('_time_constraint', $this->requestStack->getMainRequest()->get('_time_constraint', true));
+        $activityYearFilter->setActivityYearConstraint($this->activityYearConstraint);
     }
 }

+ 0 - 68
src/EventListener/Organization/OrganizationChangedSubscriber.php

@@ -1,68 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\EventListener\Organization;
-
-use App\Entity\Billing\BillingSetting;
-use App\Entity\Organization\Organization;
-use App\Entity\Organization\Parameters;
-use App\Enum\Organization\LegalEnum;
-use App\EventListener\Helper;
-use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface;
-use Doctrine\ORM\EntityManagerInterface;
-use Doctrine\ORM\Event\OnFlushEventArgs;
-use Doctrine\ORM\Events;
-
-/**
- * Classe subscriber qui doit intervenir quand on flush une entité Organization
- */
-class OrganizationChangedSubscriber implements EventSubscriberInterface
-{
-    use Helper;
-
-    /**
-     * On souscrit à l'événement OnFlush
-     * @return array
-     */
-    public function getSubscribedEvents(): array
-    {
-        return [
-            Events::onFlush
-        ];
-    }
-
-    /**
-     * onFlush Event
-     * @param OnFlushEventArgs $onFlushEventArgs
-     */
-    public function onFlush(OnFlushEventArgs $onFlushEventArgs){
-        $entityManager = $onFlushEventArgs->getEntityManager();
-        $uow = $entityManager->getUnitOfWork();
-        foreach ($uow->getScheduledEntityUpdates() as $entityUpdate){
-            if($entityUpdate instanceof Organization){
-                //Si dans l'update de l'entité, on modifie le champs "legalStatus"
-                if($this->hasChangeField($entityManager, $entityUpdate, 'legalStatus'))
-                    $this->handleLegalStatusChanged($entityUpdate, $entityManager);
-            }
-        }
-    }
-
-    /**
-     * Changement qui doivent être fait si le statut légale d'une structure est changé.
-     * @param Organization $organization
-     * @param EntityManagerInterface $entityManager
-     */
-    public function handleLegalStatusChanged(Organization $organization, EntityManagerInterface $entityManager){
-        //Si le nouveau status légal n'est pas "Association Loi 1901"
-        if($organization->getLegalStatus() !== LegalEnum::ASSOCIATION_LAW_1901()->getValue()){
-            $organization->getParameters()->setShowAdherentList(false);
-            $entityManager->getUnitOfWork()->computeChangeSet($entityManager->getClassMetadata(Parameters::class), $organization->getParameters());
-        }
-
-        //Si le nouveau status légal est "Société commerciale"
-        if($organization->getLegalStatus() === LegalEnum::COMMERCIAL_SOCIETY()->getValue()){
-            $organization->getBillingSetting()->setApplyVat(true);
-            $entityManager->getUnitOfWork()->computeChangeSet($entityManager->getClassMetadata(BillingSetting::class), $organization->getBillingSetting());
-        }
-    }
-}

+ 61 - 0
src/Filter/DoctrineFilter/AbstractTimeFilter.php

@@ -0,0 +1,61 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Filter\DoctrineFilter;
+
+use App\Annotation\DateTimeConstraintAware;
+use App\Service\Constraint\DateTimeConstraint;
+use App\Service\Utils\StringsUtils;
+use App\Tests\Filter\DoctrineFilter\DateTimeFilterTest;
+use Doctrine\ORM\Mapping\ClassMetadata;
+use Doctrine\ORM\Query\Filter\SQLFilter;
+
+/**
+ * Classe AbstractTimeFilterUtils fournie des méthodes pour construire les requetes de contrainte de temps
+ */
+abstract class AbstractTimeFilter extends SQLFilter
+{
+    /**
+     * Fonction permettant de construire la requête SQL correspondante aux contraintes
+     * @param array $constraints
+     * @param string $targetTableAlias
+     * @param array $fields
+     * @return string
+     */
+    protected function constructQuery(array $constraints, string $targetTableAlias, array $fields): string{
+        $queryConditionsAND = [];
+        foreach ($constraints as $key => $constraint) {
+            $queryConditionsOR = [];
+            foreach ($constraint as $date => $conditions){
+                foreach ($conditions as $condition){
+                    $arithmetic = $this->getArithmeticValue($condition);
+                    if(!is_null($arithmetic))
+                        $queryConditionsOR[] = sprintf("%s.%s %s '%s'", $targetTableAlias, $fields[$key], $arithmetic, $date);
+                    else
+                        $queryConditionsOR[] = sprintf("%s.%s IS NULL", $targetTableAlias, $fields[$key]);
+                }
+            }
+            if(!empty($queryConditionsOR))
+                $queryConditionsAND[] = sprintf("(%s)", join(' OR ', $queryConditionsOR));
+        }
+
+        return implode(" AND ", $queryConditionsAND);
+    }
+
+    /**
+     * Fonction retournant la valeur arithmétique correspondant à la condition de la contrainte
+     * @param $condition
+     * @return string|null
+     * @see DateTimeFilterTest::testGetArithmeticValue()
+     */
+    protected function getArithmeticValue($condition): string|null{
+        switch ($condition){
+            case DateTimeConstraint::INF : return '<';
+            case DateTimeConstraint::EQUAL :  return '=';
+            case DateTimeConstraint::SUP :  return '>';
+            case DateTimeConstraint::INF + DateTimeConstraint::EQUAL : return '<=';
+            case DateTimeConstraint::SUP + DateTimeConstraint::EQUAL  : return '>=';
+        }
+        return null;
+    }
+}

+ 60 - 0
src/Filter/DoctrineFilter/ActivityYearFilter.php

@@ -0,0 +1,60 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Filter\DoctrineFilter;
+
+use App\Annotation\ActivityYearConstraintAware;
+use App\Service\Constraint\ActivityYearConstraint;
+use App\Service\Utils\StringsUtils;
+use Doctrine\ORM\Mapping\ClassMetadata;
+
+/**
+ * Classe DateTimeFilter qui définie la requête SQL devant être ajoutée aux Entités possédant l'annotation DateTimeConstraintAware
+ */
+final class ActivityYearFilter extends AbstractTimeFilter
+{
+    private ActivityYearConstraint $activityYearConstraint;
+
+    /**
+     * Méthode surchargée de SQLFilter
+     * @param ClassMetadata $targetEntity
+     * @param string $targetTableAlias
+     * @return string
+     * @throws \Exception
+     */
+    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
+    {
+        if(!$this->hasParameter('_time_constraint')
+            || !boolval(StringsUtils::unquote($this->getParameter('_time_constraint')))
+            || !$this->hasParameter('accessId')
+        )
+            return '';
+
+        $yearConstraintAware = $targetEntity->getReflectionClass()->getAttributes(ActivityYearConstraintAware::class)[0] ?? null;
+        $startFieldName = $yearConstraintAware?->getArguments()['startYearFieldName'] ?? null;
+        $endFieldName = $yearConstraintAware?->getArguments()['endYearFieldName'] ?? null;
+        if ($startFieldName === '' || is_null($startFieldName) || $endFieldName === '' || is_null($endFieldName)) {
+            return '';
+        }
+
+        $accessId = intval(StringsUtils::unquote($this->getParameter('accessId')));
+
+        $constraints = $this->activityYearConstraint->invoke($accessId);
+
+        $fields = [
+            ActivityYearConstraint::START_KEY => $startFieldName,
+            ActivityYearConstraint::END_KEY => $endFieldName
+        ];
+
+        return $this->constructQuery($constraints, $targetTableAlias, $fields);
+    }
+
+    /**
+     * Permets d'assurer l'injection de dépendance du service YearTimeConstraint
+     * @param ActivityYearConstraint $activityYearConstraint
+     *
+     */
+    public function setActivityYearConstraint(ActivityYearConstraint $activityYearConstraint){
+        $this->activityYearConstraint = $activityYearConstraint;
+    }
+}

+ 2 - 47
src/Filter/DoctrineFilter/DateTimeFilter.php

@@ -4,16 +4,14 @@ declare(strict_types=1);
 namespace App\Filter\DoctrineFilter;
 
 use App\Annotation\DateTimeConstraintAware;
-use App\Service\Utils\DateTimeConstraint;
+use App\Service\Constraint\DateTimeConstraint;
 use App\Service\Utils\StringsUtils;
-use App\Tests\Filter\DoctrineFilter\DateTimeFilterTest;
 use Doctrine\ORM\Mapping\ClassMetadata;
-use Doctrine\ORM\Query\Filter\SQLFilter;
 
 /**
  * Classe DateTimeFilter qui définie la requête SQL devant être ajoutée aux Entités possédant l'annotation DateTimeConstraintAware
  */
-final class DateTimeFilter extends SQLFilter
+final class DateTimeFilter extends AbstractTimeFilter
 {
     private DateTimeConstraint $dateTimeConstraint;
 
@@ -50,49 +48,6 @@ final class DateTimeFilter extends SQLFilter
         return $this->constructQuery($constraints, $targetTableAlias, $fields);
     }
 
-    /**
-     * Fonction permettant de construire la requête SQL correspondante aux contraintes
-     * @param array $constraints
-     * @param string $targetTableAlias
-     * @param array $fields
-     * @return string
-     */
-    private function constructQuery(array $constraints, string $targetTableAlias, array $fields): string{
-        $queryConditionsAND = [];
-        foreach ($constraints as $key => $constraint) {
-            $queryConditionsOR = [];
-            foreach ($constraint as $date => $conditions){
-                foreach ($conditions as $condition){
-                    $arithmetic = $this->getArithmeticValue($condition);
-                    if(!is_null($arithmetic))
-                        $queryConditionsOR[] = sprintf("%s.%s %s '%s'", $targetTableAlias, $fields[$key], $arithmetic, $date);
-                    else
-                        $queryConditionsOR[] = sprintf("%s.%s IS NULL", $targetTableAlias, $fields[$key]);
-                }
-            }
-            if(!empty($queryConditionsOR))
-                $queryConditionsAND[] = sprintf("(%s)", join(' OR ', $queryConditionsOR));
-        }
-        return join(" AND ", $queryConditionsAND);
-    }
-
-    /**
-     * Fonction retournant la valeur arithmétique correspondant à la condition de la contrainte
-     * @param $condition
-     * @return string|null
-     * @see DateTimeFilterTest::testGetArithmeticValue()
-     */
-    private function getArithmeticValue($condition): string|null{
-        switch ($condition){
-            case DateTimeConstraint::INF : return '<';
-            case DateTimeConstraint::EQUAL :  return '=';
-            case DateTimeConstraint::SUP :  return '>';
-            case DateTimeConstraint::INF + DateTimeConstraint::EQUAL : return '<=';
-            case DateTimeConstraint::SUP + DateTimeConstraint::EQUAL  : return '>=';
-        }
-        return null;
-    }
-
     /**
      * Permets d'assurer l'injection de dépendance du service DateTimeConstraint
      * @param DateTimeConstraint $dateTimeConstraint

+ 56 - 0
src/Filter/Utils/InFilter.php

@@ -0,0 +1,56 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Filter\Utils;
+
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\PropertyInfo\Type;
+
+final class InFilter extends AbstractContextAwareFilter {
+    protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
+    {
+        // otherwise filter is applied to order and page as well
+        if (
+            !$this->isPropertyEnabled($property, $resourceClass) ||
+            !$this->isPropertyMapped($property, $resourceClass)
+        ) {
+            return;
+        }
+
+        $alias = $queryBuilder->getRootAliases()[0];
+        $parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
+        $queryBuilder
+            ->andWhere(sprintf('%s.%s IN (:%s)', $alias, $property, $parameterName))
+            ->setParameter($parameterName, explode(',', $value));
+    }
+
+    /**
+     * API docs
+     * @param string $resourceClass
+     * @return array[]
+     */
+    public function getDescription(string $resourceClass): array
+    {
+        if (!$this->properties) {
+            return [];
+        }
+
+        $description = [];
+        foreach ($this->properties as $property => $strategy) {
+            $description["$property"] = [
+                'property' => $property,
+                'type' => Type::BUILTIN_TYPE_STRING,
+                'required' => false,
+                'swagger' => [
+                    'description' => 'Filtre permettant d\'utiliser les IN',
+                    'name' => 'In Filter',
+                    'type' => 'Utils Filter',
+                ],
+            ];
+        }
+
+        return $description;
+    }
+}

+ 17 - 0
src/Message/Command/Parameters/AverageChange.php

@@ -0,0 +1,17 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Message\Command\Parameters;
+
+/**
+ * Classe AverageChange qui contiendras le message pour AverageChange
+ */
+class AverageChange
+{
+    public function __construct(private int $parametersId)
+    { }
+
+    public function getParametersId(){
+        return $this->parametersId;
+    }
+}

+ 20 - 0
src/Message/Handler/Parameters/AverageChangeHandler.php

@@ -0,0 +1,20 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Message\Handler\Parameters;
+
+use App\Message\Command\Parameters\AverageChange;
+use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
+
+class AverageChangeHandler implements MessageHandlerInterface
+{
+    public function __construct(
+    ) {}
+
+    public function __invoke(AverageChange $averageChange)
+    {
+        /**
+         * @todo Si on utilise toujours ES, il faut ici repopuler les educationNotations qui ont changées afin de mettre les bonnes notes
+         */
+    }
+}

+ 44 - 0
src/OpenApi/OpenApiFactory.php

@@ -0,0 +1,44 @@
+<?php
+declare(strict_types=1);
+
+namespace App\OpenApi;
+
+use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
+use ApiPlatform\Core\OpenApi\OpenApi;
+use ApiPlatform\Core\OpenApi\Model;
+
+/**
+ * Permet de retirer des URLs dans la doc Open API
+ */
+final class OpenApiFactory implements OpenApiFactoryInterface
+{
+    const PATH_TO_EXCLUDE = [
+        '/api/dolibarr_bills/{id}',
+        '/api/dolibarr_contract_lines/{id}',
+        '/api/dolibarr_contracts/{ref}',
+    ];
+
+    private $decorated;
+
+    public function __construct(OpenApiFactoryInterface $decorated)
+    {
+        $this->decorated = $decorated;
+    }
+
+    public function __invoke(array $context = []): OpenApi
+    {
+        $openApi = $this->decorated->__invoke($context);
+
+        $paths = $openApi->getPaths()->getPaths();
+
+        $filteredPaths = new Model\Paths();
+        foreach ($paths as $path => $pathItem) {
+            if (in_array($path, self::PATH_TO_EXCLUDE)) {
+                continue;
+            }
+            $filteredPaths->addPath($path, $pathItem);
+        }
+
+        return $openApi->withPaths($filteredPaths);
+    }
+}

+ 16 - 0
src/Repository/Billing/ResidenceAreaRepository.php

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

+ 46 - 0
src/Repository/Booking/CourseRepository.php

@@ -0,0 +1,46 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Repository\Booking;
+
+use App\Entity\Booking\Course;
+use App\Entity\Organization\Organization;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+class CourseRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, Course::class);
+    }
+
+    /**
+     * Récupère les cours d'une organisation d'une date à une autre
+     * @param Organization $organization
+     * @param \DateTimeInterface $start
+     * @param \DateTimeInterface $end
+     * @return array
+     */
+    public function getCoursesToFrom(Organization $organization, \DateTimeInterface $start, \DateTimeInterface $end): array{
+        $this->_em->getFilters()->disable('activity_year_filter');
+
+        $queryBuilder = $this->createQueryBuilder('course');
+
+        $queryBuilder
+            ->andWhere(
+                $queryBuilder->expr()->between('course.datetimeStart', ':dateStart', ':dateEnd')
+            )
+            ->andWhere('course.organization = :organization')
+            ->setParameter('dateStart', $start)
+            ->setParameter('dateEnd', $end)
+            ->setParameter('organization', $organization)
+        ;
+
+        $results = $queryBuilder->getQuery()->getResult();
+
+        $this->_em->getFilters()->enable('activity_year_filter');
+
+        return $results;
+    }
+}

+ 4 - 4
src/Repository/Core/ContactPointRepository.php

@@ -26,9 +26,9 @@ class ContactPointRepository extends ServiceEntityRepository
      * Récupération des points de contacts d'une organization et d'un type précis
      * @param String $type
      * @param Organization $organization
-     * @return array|null
+     * @return array
      */
-    public function getByTypeAndOrganization(String $type, Organization $organization): array | null{
+    public function getByTypeAndOrganization(String $type, Organization $organization): array{
         return $this->createQueryBuilder('contact_point')
             ->innerJoin('contact_point.organization', 'organization')
             ->where('contact_point.contactType = :type')
@@ -44,9 +44,9 @@ class ContactPointRepository extends ServiceEntityRepository
      * Récupération des points de contacts d'une person et d'un type précis
      * @param String $type
      * @param Person $person
-     * @return array|null
+     * @return array
      */
-    public function getByTypeAndPerson(String $type, Person $person): array | null{
+    public function getByTypeAndPerson(String $type, Person $person): array {
         return $this->createQueryBuilder('contact_point')
             ->innerJoin('contact_point.person', 'person')
             ->where('contact_point.contactType = :type')

+ 16 - 0
src/Repository/Education/CycleRepository.php

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

+ 16 - 0
src/Repository/Education/EducationCurriculumRepository.php

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

+ 16 - 0
src/Repository/Education/EducationNotationConfigRepository.php

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

+ 16 - 0
src/Repository/Education/EducationTimingRepository.php

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

+ 1 - 1
src/Repository/Organization/OrganizationAddressPostalRepository.php

@@ -27,7 +27,7 @@ class OrganizationAddressPostalRepository extends ServiceEntityRepository
      * @param Organization $organization
      * @return array|null
      */
-    public function getByType(String $type, Organization $organization): array | null{
+    public function getByType(String $type, Organization $organization): ?array{
         return $this->createQueryBuilder('organizationAddressPostal')
             ->where('organizationAddressPostal.type = :type')
             ->andWhere('organizationAddressPostal.organization = :organization')

+ 0 - 2
src/Security/Voter/ModuleVoter.php

@@ -7,11 +7,9 @@ use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
 use App\Entity\Access\Access;
 use App\Entity\Organization\Organization;
 use App\Service\Security\Module;
-use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 use Symfony\Component\Security\Core\Authorization\Voter\Voter;
-use ApiPlatform\Core\Util\RequestAttributesExtractor;
 
 /**
  * Class ModuleVoter : permet d'assurer que la resource appelée est comprise dans l'un des modules de la structure

+ 44 - 0
src/Service/Access/AdminAccessUtils.php

@@ -0,0 +1,44 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Access;
+
+use App\ApiResources\Access\AdminAccess;
+use App\Entity\Organization\Organization;
+use App\Service\Core\ContactPointUtils;
+use App\Test\Service\Access\AdminAccessUtilsTest;
+
+/**
+ * Class AdminAccessUtils : Service contenant les manipulations associés à la ressource AdminAccess
+ * @package App\Service\Access
+ */
+class AdminAccessUtils
+{
+    public function __construct(
+        private Utils $accessUtils,
+        private ContactPointUtils $contactPointUtils
+    )
+    { }
+
+    /**
+     * Renvoi l'objet AdminAccess initialisé par rapport à l'organization passée en paramètre
+     * @param Organization $organization
+     * @return AdminAccess|null
+     * @see AdminAccessUtilsTest::testGetAdminAccess()
+     */
+    public function getAdminAccess(Organization $organization): ?AdminAccess{
+        $administrator = $this->accessUtils->getAdminAccess($organization);
+        if(!$administrator) return null;
+
+        $contactPoint = $this->contactPointUtils->getPersonContactPointPrincipal($administrator);
+        if(!$contactPoint) return null;
+
+        $adminAccess = new AdminAccess();
+        $adminAccess
+            ->setId($administrator->getId())
+            ->setUsername($administrator->getPerson()->getUsername())
+            ->setEmail($contactPoint->getEmail())
+        ;
+        return $adminAccess;
+    }
+}

+ 16 - 0
src/Service/Access/Utils.php

@@ -4,6 +4,8 @@ declare(strict_types=1);
 namespace App\Service\Access;
 
 use App\Entity\Access\Access;
+use App\Entity\Organization\Organization;
+use App\Repository\Access\AccessRepository;
 use App\Service\ServiceIterator\OptionalsRolesIterator;
 use App\Test\Service\Access\UtilsTest;
 use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
@@ -16,6 +18,7 @@ class Utils
 {
     public function __construct(
         private RoleHierarchyInterface $roleHierarchy,
+        private AccessRepository $accessRepository,
         private OptionalsRolesIterator $optionalsRolesIterator
     )
     {}
@@ -55,4 +58,17 @@ class Utils
         $roles = $this->optionalsRolesIterator->getOptionalsRoles($access);
         return $this->roleHierarchy->getReachableRoleNames(array_merge($access->getRoles(), $roles));
     }
+
+    /**
+     * Renvoi l'access de l'organization qui est le "super admin"
+     * @param Organization $organization
+     * @return Access|null
+     * @see UtilsTest::testGetAdminAccess()
+     */
+    public function getAdminAccess(Organization $organization): Access|null{
+        return $this->accessRepository->findOneBy([
+            'adminAccess' => true,
+            'organization' => $organization
+        ]) ?? null;
+    }
 }

+ 109 - 0
src/Service/Constraint/AbstractTimeConstraintUtils.php

@@ -0,0 +1,109 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Constraint;
+
+/**
+ * Classe Utils qui fournit de méthodes pour calculer les contraintes de temps
+ */
+abstract class AbstractTimeConstraintUtils
+{
+    const NULL = 0;
+    const INF = 1;
+    const EQUAL = 3;
+    const SUP = 5;
+    const CANCEL_OPERATION = 9;
+    const START_KEY = 'start';
+    const END_KEY = 'end';
+    const NULL_VALUE = 'NULL';
+
+    /**
+     * Retourne true si l'utilisateur veux une période précise
+     * @param $historical
+     * @return bool
+     * @see DateTimeConstraintTest::testHasCustomPeriods()
+     */
+    protected function hasCustomPeriods($historical): bool{
+        return array_key_exists('dateStart', $historical) && $historical['dateStart'] && array_key_exists('dateEnd', $historical) && $historical['dateEnd'];
+    }
+
+    /**
+     * Fonction permettant d'ajouter une nouvelle contrainte de date
+     * @param array $contraints
+     * @param array $newContraint
+     * @return array
+     * @see DateTimeConstraintTest::testAddConstraint()
+     */
+    protected function addConstraint(array $contraints, array $newContraint): array{
+        $contraints = $this->mergeConstraint($contraints,$newContraint,self::START_KEY);
+        $contraints = $this->mergeConstraint($contraints,$newContraint,self::END_KEY);
+        return $contraints;
+    }
+
+    /**
+     * Construit le tableau de contraintes pour une condition (start, end)
+     * @param array $contraints
+     * @param array $newContraint
+     * @param string $key
+     * @return array
+     */
+    protected function mergeConstraint(array $contraints, array $newContraint, string $key): array{
+        if(array_key_exists($key, $newContraint)){
+            foreach ($newContraint[$key] as $dateKey => $arithmeticValue){
+                //Si la date à déjà des conditions
+                if(array_key_exists($dateKey, $contraints[$key])){
+                    //Si la conditions (<, >, =, ...) n'est pas encore appliquée à la date
+                    if(!in_array($arithmeticValue, $contraints[$key][$dateKey]))
+                        $contraints[$key][$dateKey][] = $arithmeticValue;
+                }else{
+                    $contraints[$key][$dateKey] = [$arithmeticValue];
+                }
+            }
+        }
+
+        return $contraints;
+    }
+
+    /**
+     * Nettoyage des contraintes (toutes celles supérieur à la condition cancel et les valeurs null isolées)
+     * @param array $constraints
+     * @return array
+     * @see DateTimeConstraintTest::testCleanConstraints()
+     */
+    protected function cleanConstraints(array $constraints): array{
+        $constraints[self::START_KEY] = $this->filterConstraint($constraints, self::START_KEY);
+        $constraints[self::START_KEY] = $this->clearNull($constraints, self::START_KEY);
+
+        $constraints[self::END_KEY] = $this->filterConstraint($constraints, self::END_KEY);
+        $constraints[self::END_KEY] = $this->clearNull($constraints, self::END_KEY);
+        return $constraints;
+    }
+
+    /**
+     * Pour chaque contraintes appliquées à une date on vérifie qu'on ne dépasse pas la valeur cancel : c'est à dire
+     * la condition qui démontre que toutes les valeurs arithmétiques ont été choisies
+     * @param array $constraints
+     * @param $key
+     * @return array
+     * @see DateTimeConstraintTest::testFilterConstraint()
+     */
+    protected function filterConstraint(array $constraints, string $key): array{
+        return array_filter($constraints[$key], function($constraint){
+            return array_sum($constraint) < self::CANCEL_OPERATION ;
+        });
+    }
+
+    /**
+     * On ne conserve pas les contraintes sur des conditions start et end si ces dernieres ne possède qu'une valeur null :
+     * une valeur null doit obligatoirement s'appliquer avec un OR
+     * @param array $constraints
+     * @param $key
+     * @return array
+     * @see DateTimeConstraintTest::testClearNull()
+     */
+    protected function clearNull(array $constraints, string $key): array{
+        if(count($constraints[$key]) === 1 && array_key_exists(self::NULL_VALUE, $constraints[$key]))
+            $constraints[$key] = [];
+        return $constraints[$key];
+    }
+}

+ 121 - 0
src/Service/Constraint/ActivityYearConstraint.php

@@ -0,0 +1,121 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Constraint;
+
+use App\Entity\Access\Access;
+use App\Service\Organization\Utils as organizationUtils;
+use Doctrine\ORM\EntityManagerInterface;
+
+/**
+ * Classe ActivityYearConstraint qui définie l'année de début (et de fin dans le cas d'une période custom)
+ * par rapport au contraintes temporelles choisies par un utilisateur.
+ */
+class ActivityYearConstraint extends AbstractTimeConstraintUtils
+{
+    public function __construct(
+        private EntityManagerInterface $entityManager,
+    )
+    { }
+
+    /**
+     * Main méthode
+     * @param int $accessID
+     * @return array
+     * @throws \Exception
+     */
+    public function invoke(int $accessID): array
+    {
+        $access = $this->entityManager->getRepository(Access::class)->find($accessID);
+        $historical = $access->getHistorical();
+
+        $contraints = [
+            self::START_KEY => [],
+            self::END_KEY => []
+        ];
+
+        if($this->hasCustomPeriods($historical)){
+            $periods = $this->getRangeYear($access, $historical['dateStart'], $historical['dateEnd']);
+            $contraints = $this->addConstraint($contraints, $this->customConstraint($periods));
+        }else{
+            $year = $access->getActivityYear();
+            if($historical['present']) $contraints = $this->addConstraint($contraints, $this->presentConstraint($year));
+            if($historical['past']) $contraints = $this->addConstraint($contraints, $this->pastConstraint($year));
+            if($historical['future']) $contraints = $this->addConstraint($contraints, $this->futurConstraint($year));
+        }
+        return $this->cleanConstraints($contraints);
+    }
+
+    /**
+     * Retourne le tableau des années comprise dans la période custom
+     * @param Access $access
+     * @param string $dateStart
+     * @param string $dateEnd
+     * @return string[]
+     */
+    private function getRangeYear(Access $access, string $dateStart, string $dateEnd): array{
+        $organization = $access->getOrganization();
+        return [
+            organizationUtils::START_DATE_KEY => organizationUtils::getActivityYearSwitchDate($organization, new \DateTime($dateStart)),
+            organizationUtils::END_DATE_KEY => organizationUtils::getActivityYearSwitchDate($organization, new \DateTime($dateEnd))
+        ];
+    }
+
+    /**
+     * Une période est dans le présent si :
+     * - l'année de début est égale (=) à l'année à afficher
+     * @param $year
+     * @return array
+     */
+    private function presentConstraint(int $year): array{
+        return [
+          self::START_KEY => [
+              $year => self::EQUAL
+          ]
+        ];
+    }
+
+    /**
+     * Une période est dans le passée si :
+     * - l'année de début est plus petite (<) à l'année à afficher
+     * @param $year
+     * @return array
+     */
+    private function pastConstraint($year): array{
+        return [
+            self::END_KEY => [
+                $year => self::INF
+            ]
+        ];
+    }
+
+    /**
+     * Une période est dans le future si :
+     * - l'année de début est plus grande (>) à l'année à afficher
+     * @param $year
+     * @return array
+     */
+    private function futurConstraint($year): array{
+        return [
+            self::START_KEY => [
+                $year => self::SUP
+            ]
+        ];
+    }
+
+    /**
+     * Une période est dans une contrainte custom si :
+     * - l'année de début est plus grande ou égale (>=) à l'année de départ
+     * - l'année de début est plus petite ou égale (<=) à l'année de fin
+     * @param $year
+     * @return array
+     */
+    private function customConstraint($years): array{
+        return [
+            self::START_KEY => [
+                $years[organizationUtils::START_DATE_KEY]  => self::SUP + self::EQUAL,
+                $years[organizationUtils::END_DATE_KEY] => self::INF + self::EQUAL
+            ]
+        ];
+    }
+}

+ 3 - 102
src/Service/Utils/DateTimeConstraint.php → src/Service/Constraint/DateTimeConstraint.php

@@ -1,28 +1,19 @@
 <?php
 declare(strict_types=1);
 
-namespace App\Service\Utils;
+namespace App\Service\Constraint;
 
 use App\Entity\Access\Access;
 use App\Service\Organization\Utils as organizationUtils;
-use App\Tests\Service\Utils\DateTimeConstraintTest;
+use App\Tests\Service\Constraint\DateTimeConstraintTest;
 use Doctrine\ORM\EntityManagerInterface;
 
 /**
  * Classe DateTimeConstraint qui définie les dates de débuts et fin de périodes
  * par rapport au contraintes temporelles choisies par un utilisateur.
  */
-class DateTimeConstraint
+class DateTimeConstraint extends AbstractTimeConstraintUtils
 {
-    const NULL = 0;
-    const INF = 1;
-    const EQUAL = 3;
-    const SUP = 5;
-    const CANCEL_OPERATION = 9;
-    const START_KEY = 'start';
-    const END_KEY = 'end';
-    const NULL_VALUE = 'NULL';
-
     public function __construct(
         private EntityManagerInterface $entityManager,
     )
@@ -58,16 +49,6 @@ class DateTimeConstraint
         return $this->cleanConstraints($contraints);
     }
 
-    /**
-     * Retourne true si l'utilisateur veux une période précise
-     * @param $historical
-     * @return bool
-     * @see DateTimeConstraintTest::testHasCustomPeriods()
-     */
-    private function hasCustomPeriods($historical): bool{
-        return array_key_exists('dateStart', $historical) && $historical['dateStart'] && array_key_exists('dateEnd', $historical) && $historical['dateEnd'];
-    }
-
     /**
      * Retourne le tableau des périodes custom
      * @param string $dateStart
@@ -103,86 +84,6 @@ class DateTimeConstraint
         return $periods;
     }
 
-    /**
-     * Fonction permettant d'ajouter une nouvelle contrainte de date
-     * @param array $contraints
-     * @param array $newContraint
-     * @return array
-     * @see DateTimeConstraintTest::testAddConstraint()
-     */
-    private function addConstraint(array $contraints, array $newContraint): array{
-        $contraints = $this->mergeConstraint($contraints,$newContraint,self::START_KEY);
-        $contraints = $this->mergeConstraint($contraints,$newContraint,self::END_KEY);
-        return $contraints;
-    }
-
-    /**
-     * Construit le tableau de contraintes pour une condition (start, end)
-     * @param array $contraints
-     * @param array $newContraint
-     * @param string $key
-     * @return array
-     */
-    private function mergeConstraint(array $contraints, array $newContraint, string $key): array{
-        if(array_key_exists($key, $newContraint)){
-            foreach ($newContraint[$key] as $dateKey => $arithmeticValue){
-                //Si la date à déjà des conditions
-                if(array_key_exists($dateKey, $contraints[$key])){
-                    //Si la conditions (<, >, =, ...) n'est pas encore appliquée à la date
-                    if(!in_array($arithmeticValue, $contraints[$key][$dateKey]))
-                        $contraints[$key][$dateKey][] = $arithmeticValue;
-                }else{
-                    $contraints[$key][$dateKey] = [$arithmeticValue];
-                }
-            }
-        }
-
-        return $contraints;
-    }
-
-    /**
-     * Nettoyage des contraintes (toutes celles supérieur à la condition cancel et les valeurs null isolées)
-     * @param array $constraints
-     * @return array
-     * @see DateTimeConstraintTest::testCleanConstraints()
-     */
-    private function cleanConstraints(array $constraints): array{
-        $constraints[self::START_KEY] = $this->filterConstraint($constraints, self::START_KEY);
-        $constraints[self::START_KEY] = $this->clearNull($constraints, self::START_KEY);
-
-        $constraints[self::END_KEY] = $this->filterConstraint($constraints, self::END_KEY);
-        $constraints[self::END_KEY] = $this->clearNull($constraints, self::END_KEY);
-        return $constraints;
-    }
-
-    /**
-     * Pour chaque contraintes appliquées à une date on vérifie qu'on ne dépasse pas la valeur cancel : c'est à dire
-     * la condition qui démontre que toutes les valeurs arithmétiques ont été choisies
-     * @param array $constraints
-     * @param $key
-     * @return array
-     * @see DateTimeConstraintTest::testFilterConstraint()
-     */
-    private function filterConstraint(array $constraints, string $key): array{
-        return array_filter($constraints[$key], function($constraint){
-            return array_sum($constraint) < self::CANCEL_OPERATION ;
-        });
-    }
-
-    /**
-     * On ne conserve pas les contraintes sur des conditions start et end si ces dernieres ne possède qu'une valeur null :
-     * une valeur null doit obligatoirement s'appliquer avec un OR
-     * @param array $constraints
-     * @param $key
-     * @return array
-     * @see DateTimeConstraintTest::testClearNull()
-     */
-    private function clearNull(array $constraints, string $key): array{
-        if(count($constraints[$key]) === 1 && array_key_exists(self::NULL_VALUE, $constraints[$key]))
-            $constraints[$key] = [];
-        return $constraints[$key];
-    }
-
     /**
      * Une période est dans le présent si :
      *  - la date de début est plus petite ou égale (<=) à la date de fin de période à afficher

+ 35 - 0
src/Service/Core/ContactPointUtils.php

@@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Core;
+
+use App\Entity\Access\Access;
+use App\Entity\Core\ContactPoint;
+use App\Enum\Core\ContactPointTypeEnum;
+use App\Repository\Core\ContactPointRepository;
+use App\Test\Service\Access\ContactPointUtilsTest;
+
+
+/**
+ * Classe ContactPointUtils qui possédant les fonctions utils à l'entité ContactPoint
+ */
+class ContactPointUtils
+{
+    public function __construct(private ContactPointRepository $contactPointRepository){
+    }
+
+    /**
+     * Renvoie le point de contact principal de l'Access passé en paramètre
+     * @param Access $access
+     * @return ContactPoint|null
+     * @see ContactPointUtilsTest::testGetPersonContactPointPrincipal()
+     */
+    public function getPersonContactPointPrincipal(Access $access): ?ContactPoint{
+        $contactPoint = $this->contactPointRepository->getByTypeAndPerson(ContactPointTypeEnum::PRINCIPAL()->getValue(), $access->getPerson());
+        if(count($contactPoint) === 0) return null;
+        if(count($contactPoint) !== 1){
+            throw new \Exception('more_than_one_result');
+        }
+        return $contactPoint[0];
+    }
+}

+ 21 - 1
src/Service/Mobyt/MobytService.php

@@ -4,7 +4,9 @@ declare(strict_types=1);
 namespace App\Service\Mobyt;
 
 use App\Service\Rest\ApiRequestService;
+use App\Tests\Service\Mobyt\MobytServiceTest;
 use JetBrains\PhpStorm\Pure;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Contracts\HttpClient\HttpClientInterface;
 
 /**
@@ -37,7 +39,7 @@ class MobytService extends ApiRequestService
     }
 
     /**
-     * Get a dolibarr society by its opentalent organization id
+     * Get a mobyt user status by its opentalent organization id
      *
      * @param int $organizationId
      * @param string $login
@@ -54,4 +56,22 @@ class MobytService extends ApiRequestService
             ]
         );
     }
+
+    /**
+     * Test if login and pass are correct
+     *
+     * @param string $login
+     * @param string $password
+     * @return bool
+     * @see MobytServiceTest::testHasCredentialsCorrect()
+     */
+    public function hasCredentialsCorrect(string $login, string $password): bool
+    {
+        try{
+            $this->connect($login, $password);
+        }catch (NotFoundHttpException $exception){
+            return false;
+        }
+        return true;
+    }
 }

+ 29 - 0
src/Service/OnChange/Organization/OnOrganizationChange.php

@@ -0,0 +1,29 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\OnChange\Organization;
+
+use App\Entity\Organization\Organization;
+use App\Enum\Organization\LegalEnum;
+use App\Test\Service\OnChange\Organization\OnOrganizationChangeTest;
+
+/**
+ * Classe OnOrganizationChange qui comporte toutes les opérations automatiquent se produisant lors de l'évolution d'une organisation
+ */
+class OnOrganizationChange
+{
+    /**
+     * Si le statut de l'organization évolue, on doit faire évoluer d'autre paramètres.
+     * @param Organization $organization
+     * @see OnOrganizationChangeTest::testOnLegalStatusChange()
+     */
+    public function onLegalStatusChange(Organization $organization): void{
+        if($organization->getLegalStatus() !== LegalEnum::ASSOCIATION_LAW_1901()->getValue()){
+            $organization->getParameters()->setShowAdherentList(false);
+        }
+
+        if($organization->getLegalStatus() === LegalEnum::COMMERCIAL_SOCIETY()->getValue()){
+            $organization->getBillingSetting()->setApplyVat(true);
+        }
+    }
+}

+ 77 - 0
src/Service/OnChange/Organization/OnParametersChange.php

@@ -0,0 +1,77 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\OnChange\Organization;
+
+use App\Entity\Booking\Course;
+use App\Entity\Education\EducationNotationConfig;
+use App\Entity\Organization\Organization;
+use App\Entity\Organization\Parameters;
+use App\Enum\Education\AdvancedEducationNotationTypeEnum;
+use App\Repository\Booking\CourseRepository;
+use App\Test\Service\OnChange\Organization\OnParametersChangeTest;
+use App\Service\Organization\Utils as organizationUtils;
+
+/**
+ * Classe OnParametersChange qui comporte toutes les opérations automatiquent se produisant lors de l'évolution des paramètres
+ */
+class OnParametersChange
+{
+    public function __construct(
+        private CourseRepository $courseRepository
+    ){ }
+
+    /**
+     * Si le le type de grilles d'évaluation évolue, il faut "nettoyer" les curriculums/teachers associés au type précédent
+     * @param Parameters $parameters
+     * @see OnParametersChangeTest::testOnAdvancedEducationNotationTypeByTeachersChange()
+     * @see OnParametersChangeTest::testOnAdvancedEducationNotationTypeByEducationChange()
+     */
+    public function onAdvancedEducationNotationTypeChange(Parameters $parameters): void {
+        foreach ($parameters->getOrganization()->getEducationNotationConfigs() as $educationNotationConfig){
+            /** @var EducationNotationConfig $educationNotationConfig */
+            if($parameters->getAdvancedEducationNotationType() === AdvancedEducationNotationTypeEnum::BY_TEACHER()->getValue()){
+                foreach ($educationNotationConfig->getEducationCurriculums() as $educationCurriculum){
+                    $educationCurriculum->setEducationNotationConfig(null);
+                }
+            }else{
+                foreach ($educationNotationConfig->getTeachers() as $teacher){
+                    $teacher->setEducationNotationConfig(null);
+                }
+            }
+        }
+    }
+
+    /**
+     * Permet de mettre à jour l'année de début des cours concernés suivant la date de l'activité musicale qui vient d'être changés
+     * @param Organization $organization
+     * @param \DateTimeInterface $previousMusicalDate
+     * @param \DateTimeInterface $newMusicalDate
+     * @throws \Exception
+     * @see OnParametersChangeTest::testOnMusicalDateChange()
+     */
+    public function onMusicalDateChange(Organization $organization, \DateTimeInterface $previousMusicalDate): void{
+        $currentMusicalDate = $organization->getParameters()->getMusicalDate();
+
+        if($previousMusicalDate > $currentMusicalDate){
+            $lowerDate = $currentMusicalDate;
+            $higherDate = $previousMusicalDate;
+        }else{
+            $lowerDate = $previousMusicalDate;
+            $higherDate = $currentMusicalDate;
+        }
+
+        $startDate = new \DateTime(date('Y') . '-' . $lowerDate->format('m-d'));
+        $endDate = new \DateTime(date('Y') . '-' . $higherDate->format('m-d'));
+
+        $coursesToUpdate = $this->courseRepository->getCoursesToFrom($organization, $startDate, $endDate);
+
+        /** @var Course $course */
+        foreach ($coursesToUpdate as $course){
+            $year = organizationUtils::getActivityYearSwitchDate($organization, $course->getDatetimeStart());
+            $course->setStartYear($year);
+            $course->setEndYear($year + 1);
+        }
+    }
+}

+ 1 - 0
src/Service/Organization/OrganizationProfileCreator.php

@@ -33,6 +33,7 @@ class OrganizationProfileCreator
         $organizationProfile = $this->createLightOrganizationProfile($organization);
         $organizationProfile->setModules($this->module->getOrganizationModules($organization));
         $organizationProfile->setProduct($organization->getSettings()->getProduct());
+        $organizationProfile->setParametersId($organization->getParameters()->getId());
         $organizationProfile->setLegalStatus($organization->getLegalStatus());
         $organizationProfile->setHasChildren($organization->getNetworkOrganizationChildren()->count() > 1);
         $organizationProfile->setShowAdherentList($organization->getParameters()->getShowAdherentList() && $organization->getPrincipalType() != PrincipalTypeEnum::ARTISTIC_EDUCATION_ONLY());

+ 27 - 0
src/Service/Organization/Utils.php

@@ -97,6 +97,7 @@ class Utils
      * @param int $year
      * @return \DateTime[]
      * @throws \Exception
+     * @see UtilsTest::testGetActivityPeriodsSwitchYear()
      */
     public static function getActivityPeriodsSwitchYear(Organization $organization, int $year): array
     {
@@ -116,4 +117,30 @@ class Utils
             self::END_DATE_KEY => $dateEnd->format('Y-m-d')
         ];
     }
+
+    /**
+     * Permet de retrouver quelle sera la date d'activité correspondant à une date précise de l'année
+     * @param Organization $organization
+     * @param \DateTimeInterface $date
+     * @return int
+     * @throws \Exception
+     * @see UtilsTest::testgetActivityYearSwitchDate()
+     */
+    public static function getActivityYearSwitchDate(Organization $organization, \DateTimeInterface $date): int
+    {
+        $year = $date->format('Y');
+        $musicalDate = $organization->getParameters()->getMusicalDate();
+
+        if (empty($musicalDate)) {
+            $startDate = new \DateTime($year.'-08-31');
+        }else{
+            $startDate = new \DateTime($musicalDate->format($year.'-m-d'));
+        }
+
+        $pivotDate = new \DateTime($year . '-01-01');
+
+        if($date >= $pivotDate && $date < $startDate)
+            return  (int) ($year - 1);
+        else return (int) $year;
+    }
 }

+ 0 - 1
src/Service/ServiceIterator/CurrentAccessExtensionIterator.php

@@ -17,6 +17,5 @@ class CurrentAccessExtensionIterator{
             if($extension->support($operationName))
                 return $extension->addWhere($queryBuilder);
         }
-        throw new Exception('no extension found for this operation');
     }
 }

+ 16 - 0
src/Service/Utils/EntityUtils.php

@@ -3,6 +3,7 @@ declare(strict_types=1);
 
 namespace App\Service\Utils;
 
+use App\Annotation\BillingSettingDefaultValue;
 use App\Annotation\OrganizationDefaultValue;
 use App\Entity\Access\Access;
 
@@ -15,6 +16,7 @@ class EntityUtils
     public function defaultValueSettersByAccess($entity, Access $access)
     {
         $this->organizationDefaultValue($entity, $access);
+        $this->billingSettingDefaultValueDefaultValue($entity, $access);
     }
 
     /**
@@ -30,4 +32,18 @@ class EntityUtils
             $entity->{sprintf('set%s', ucfirst($fieldName))}(...[$access->getOrganization()]);
         }
     }
+
+    /**
+     * @param $entity
+     * @throws \ReflectionException
+     */
+    private function billingSettingDefaultValueDefaultValue($entity, Access $access)
+    {
+        $reflection = new \ReflectionClass($entity::class);
+        $billingSettingDefaultValueDefault = $reflection->getAttributes(BillingSettingDefaultValue::class)[0] ?? null;
+        $fieldName = $billingSettingDefaultValueDefault?->getArguments()['fieldName'] ?? null;
+        if($fieldName){
+            $entity->{sprintf('set%s', ucfirst($fieldName))}(...[$access->getOrganization()->getBillingSetting()]);
+        }
+    }
 }

+ 17 - 0
src/Validator/Organization/Parameters/MobytCredentials.php

@@ -0,0 +1,17 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Validator\Organization\Parameters;
+
+use Symfony\Component\Validator\Constraint;
+
+#[\Attribute]
+class MobytCredentials extends Constraint
+{
+    public $message = 'wrong_mobyt_credentials';
+
+    public function getTargets()
+    {
+        return self::CLASS_CONSTRAINT;
+    }
+}

+ 37 - 0
src/Validator/Organization/Parameters/MobytCredentialsValidator.php

@@ -0,0 +1,37 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Validator\Organization\Parameters;
+
+use App\Entity\Organization\Parameters;
+use App\Service\Mobyt\MobytService;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Classe control que les logins/mot de passe mobyt sont OK.
+ */
+class MobytCredentialsValidator extends ConstraintValidator
+{
+    public function __construct(private MobytService $mobytService){}
+
+    public function validate($value, Constraint $constraint)
+    {
+        /** @var Parameters $parameters */
+        $parameters = $value;
+
+        $userNameSms = $parameters->getUsernameSMS();
+        $passwordSms = $parameters->getPasswordSMS();
+
+        if(empty($userNameSms) || empty($passwordSms))
+            return;
+
+        try{
+            $this->mobytService->hasCredentialsCorrect($userNameSms, $passwordSms);
+        }catch (\Exception $exception){
+            $this->context->buildViolation($constraint->message)
+                ->atPath('passwordSMS')
+                ->addViolation();
+        }
+    }
+}

+ 87 - 0
tests/Service/Access/AdminAccessUtilsTest.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace App\Test\Service\Access;
+
+use App\ApiResources\Access\AdminAccess;
+use App\Entity\Access\Access;
+use App\Entity\Core\ContactPoint;
+use App\Entity\Organization\Organization;
+use App\Entity\Person\Person;
+use App\Service\Access\AdminAccessUtils;
+use App\Service\Access\Utils;
+use App\Service\Core\ContactPointUtils;
+use PHPUnit\Framework\TestCase;
+
+class AdminAccessUtilsTest extends TestCase
+{
+    private Organization $organization;
+    private AdminAccessUtils  $adminAccessUtils;
+    private ContactPointUtils $contactPointUtilsMock;
+    private Utils $accessUtilsMock;
+
+    public function setUp():void
+    {
+        $this->organization = new Organization();
+
+        $this->accessUtilsMock = $this->getMockBuilder(Utils::class)->disableOriginalConstructor()->getMock();
+        $this->contactPointUtilsMock = $this->getMockBuilder(ContactPointUtils::class)->disableOriginalConstructor()->getMock();
+
+        $this->adminAccessUtils = new AdminAccessUtils(
+            $this->accessUtilsMock,
+            $this->contactPointUtilsMock
+        );
+    }
+
+    /**
+     * @see AdminAccessUtils::getAdminAccess()
+     */
+    public function testGetAdminAccessWithoutAdministrator(){
+        $this->accessUtilsMock
+            ->method('getAdminAccess')
+            ->with($this->organization)
+            ->willReturn(null);
+
+        $this->assertNull($this->adminAccessUtils->getAdminAccess($this->organization));
+    }
+
+    /**
+     * @see AdminAccessUtils::getAdminAccess()
+     */
+    public function testGetAdminAccessWithoutContactPoint(){
+        $administrator = new Access();
+
+        $this->accessUtilsMock
+            ->method('getAdminAccess')
+            ->with($this->organization)
+            ->willReturn($administrator);
+
+        $this->contactPointUtilsMock
+            ->method('getPersonContactPointPrincipal')
+            ->with($administrator)
+            ->willReturn(null);
+
+        $this->assertNull($this->adminAccessUtils->getAdminAccess($this->organization));
+    }
+
+    /**
+     * @see AdminAccessUtils::getAdminAccess()
+     */
+    public function testGetAdminAccess(){
+        $person = new Person();
+        $administrator = new Access();
+        $administrator->setPerson($person);
+        $contactPoint = new ContactPoint();
+
+        $this->accessUtilsMock
+            ->method('getAdminAccess')
+            ->with($this->organization)
+            ->willReturn($administrator);
+
+        $this->contactPointUtilsMock
+            ->method('getPersonContactPointPrincipal')
+            ->with($administrator)
+            ->willReturn($contactPoint);
+
+        $this->assertInstanceOf(AdminAccess::class, $this->adminAccessUtils->getAdminAccess($this->organization));
+    }
+}

+ 30 - 1
tests/Service/Access/UtilsTest.php

@@ -3,6 +3,9 @@
 namespace App\Test\Service\Access;
 
 use App\Entity\Access\Access;
+use App\Entity\Organization\Organization;
+use App\Repository\Access\AccessRepository;
+use App\Service\Access\HandleOptionalsRoles;
 use App\Service\Access\Utils;
 use App\Service\ServiceIterator\OptionalsRolesIterator;
 use PHPUnit\Framework\TestCase;
@@ -11,6 +14,7 @@ use Symfony\Component\Security\Core\Role\RoleHierarchy;
 class UtilsTest extends TestCase
 {
     private Utils $utils;
+    private AccessRepository $accessRepositoryMock;
 
     public function setUp():void
     {
@@ -19,12 +23,15 @@ class UtilsTest extends TestCase
             ->method('getReachableRoleNames')
             ->willReturn(["ROLE_A", "ROLE_B"]);
 
+
         $optionalsRolesIteratorMock = $this->getMockBuilder(OptionalsRolesIterator::class)->disableOriginalConstructor()->getMock();
         $optionalsRolesIteratorMock
             ->method('getOptionalsRoles')
             ->willReturn(["ROLE_OPT"]);
 
-        $this->utils = new Utils($roleHierarchyMock, $optionalsRolesIteratorMock);
+        $this->accessRepositoryMock = $this->getMockBuilder(AccessRepository::class)->disableOriginalConstructor()->getMock();
+
+        $this->utils = new Utils($roleHierarchyMock,  $this->accessRepositoryMock, $optionalsRolesIteratorMock);
     }
 
     /**
@@ -86,4 +93,26 @@ class UtilsTest extends TestCase
 
         $this->assertEquals(['ROLE_A', 'ROLE_B'], $this->utils->getAllRoles($accessMock1));
     }
+
+    /**
+     * @see Utils::getAdminAccess()
+     */
+    public function testGetAdminAccess(){
+        $this->accessRepositoryMock
+            ->method('findOneBy')
+            ->willReturn(new Access());
+
+        $this->assertNotEmpty($this->utils->getAdminAccess(new Organization()));
+    }
+
+    /**
+     * @see Utils::getAdminAccess()
+     */
+    public function testGetAdminAccessNotFound(){
+        $this->accessRepositoryMock
+            ->method('findOneBy')
+            ->willReturn(null);
+
+        $this->assertNull($this->utils->getAdminAccess(new Organization()));
+    }
 }

+ 2 - 2
tests/Service/Utils/DateTimeConstraintTest.php → tests/Service/Constraint/DateTimeConstraintTest.php

@@ -1,10 +1,10 @@
 <?php
-namespace App\Tests\Service\Utils;
+namespace App\Tests\Service\Constraint;
 
 use App\Entity\Access\Access;
 use App\Entity\Organization\Organization;
 use App\Entity\Organization\Parameters;
-use App\Service\Utils\DateTimeConstraint;
+use App\Service\Constraint\DateTimeConstraint;
 use App\Tests\TestToolsTrait;
 use Doctrine\ORM\EntityManagerInterface;
 use PHPUnit\Framework\TestCase;

+ 57 - 0
tests/Service/Core/ContactPointUtilsTest.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace App\Test\Service\Access;
+
+use App\Entity\Access\Access;
+use App\Entity\Core\ContactPoint;
+use App\Entity\Person\Person;
+use App\Enum\Core\ContactPointTypeEnum;
+use App\Repository\Core\ContactPointRepository;
+use App\Service\Access\Utils;
+use App\Service\Core\ContactPointUtils;
+use PHPUnit\Framework\TestCase;
+
+class ContactPointUtilsTest extends TestCase
+{
+    private ContactPointUtils $contactPointUtils;
+    private ContactPointRepository $contactPointRepositoryMock;
+
+    public function setUp():void
+    {
+        $this->contactPointRepositoryMock = $this->getMockBuilder(ContactPointRepository::class)->disableOriginalConstructor()->getMock();
+
+        $this->contactPointUtils = new ContactPointUtils($this->contactPointRepositoryMock);
+    }
+
+    /**
+     * @see Utils::getPersonContactPointPrincipal()
+     */
+    public function testGetPersonContactPointPrincipal(){
+        $person = new Person();
+        $access = new Access();
+        $access->setPerson($person);
+
+        $this->contactPointRepositoryMock
+            ->method('getByTypeAndPerson')
+            ->with(ContactPointTypeEnum::PRINCIPAL()->getValue(), $access->getPerson())
+            ->willReturn([new ContactPoint()]);
+
+        $this->assertInstanceOf(ContactPoint::class, $this->contactPointUtils->getPersonContactPointPrincipal($access));
+    }
+
+    /**
+     * @see Utils::getPersonContactPointPrincipal()
+     */
+    public function testGetPersonContactPointPrincipalNotExist(){
+        $person = new Person();
+        $access = new Access();
+        $access->setPerson($person);
+
+        $this->contactPointRepositoryMock
+            ->method('getByTypeAndPerson')
+            ->with(ContactPointTypeEnum::PRINCIPAL()->getValue(), $access->getPerson())
+            ->willReturn([]);
+
+        $this->assertNull($this->contactPointUtils->getPersonContactPointPrincipal($access));
+    }
+}

+ 43 - 0
tests/Service/Mobyt/MobytServiceTest.php

@@ -4,6 +4,7 @@ namespace App\Tests\Service\Mobyt;
 
 use App\Service\Mobyt\MobytService;
 use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Contracts\HttpClient\HttpClientInterface;
 use Symfony\Contracts\HttpClient\ResponseInterface;
 
@@ -62,4 +63,46 @@ class MobytServiceTest extends TestCase
             33.0
         );
     }
+
+    /**
+     * @see MobytService::hasCredentialsCorrect()
+     */
+    public function testHasCredentialsCorrect(): void {
+
+        // Mock the response that will be returned by the connection request
+        $connectResponse = $this->getMockBuilder(ResponseInterface::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $connectResponse->method('getContent')->willReturn('userId;sessionKey');
+
+        $this->client
+            ->method('request')
+            ->with("GET", "login?username=user&password=pwd")
+            ->willReturnOnConsecutiveCalls(
+                $connectResponse
+            );
+
+        $this->assertTrue($this->mobytService->hasCredentialsCorrect('user', 'pwd'));
+    }
+
+    /**
+     * @see MobytService::hasCredentialsCorrect()
+     */
+    public function testHasCredentialsUnCorrect(): void {
+
+        // Mock the response that will be returned by the connection request
+        $connectResponse = $this->getMockBuilder(ResponseInterface::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $connectResponse->method('getContent')->willThrowException(new NotFoundHttpException());
+
+        $this->client
+            ->method('request')
+            ->with("GET", "login?username=user&password=pwd")
+            ->willReturnOnConsecutiveCalls(
+                $connectResponse
+            );
+
+        $this->assertFalse($this->mobytService->hasCredentialsCorrect('user', 'pwd'));
+    }
 }

+ 39 - 0
tests/Service/OnChange/Organization/OnOrganizationChangeTest.php

@@ -0,0 +1,39 @@
+<?php
+namespace App\Test\Service\OnChange\Organization;
+
+use App\Entity\Billing\BillingSetting;
+use App\Entity\Organization\Organization;
+use App\Entity\Organization\Parameters;
+use App\Enum\Organization\LegalEnum;
+use App\Service\OnChange\Organization\OnOrganizationChange;
+use PHPUnit\Framework\TestCase;
+
+class OnOrganizationChangeTest extends TestCase
+{
+    private Organization $organization;
+    private OnOrganizationChange $onOrganizationChange;
+
+    public function setUp():void
+    {
+        $this->organization = new Organization();
+        $this->onOrganizationChange = new OnOrganizationChange();
+    }
+
+    /**
+     * @see OnOrganizationChange::onLegalStatusChange()
+     */
+    public function testOnLegalStatusChange(){
+        $this->organization->setLegalStatus(LegalEnum::COMMERCIAL_SOCIETY()->getValue());
+        $parameters = new Parameters();
+        $parameters->setShowAdherentList(true);
+        $billingSettings = new BillingSetting();
+        $billingSettings->setApplyVat(false);
+        $this->organization->setParameters($parameters);
+        $this->organization->setBillingSetting($billingSettings);
+
+        $this->onOrganizationChange->onLegalStatusChange($this->organization);
+
+        $this->assertFalse($this->organization->getParameters()->getShowAdherentList());
+        $this->assertTrue($this->organization->getBillingSetting()->getApplyVat());
+    }
+}

+ 92 - 0
tests/Service/OnChange/Organization/OnParametersChangeTest.php

@@ -0,0 +1,92 @@
+<?php
+namespace App\Test\Service\OnChange\Organization;
+
+use App\Entity\Access\Access;
+use App\Entity\Booking\Course;
+use App\Entity\Education\EducationCurriculum;
+use App\Entity\Education\EducationNotationConfig;
+use App\Entity\Organization\Organization;
+use App\Entity\Organization\Parameters;
+use App\Enum\Education\AdvancedEducationNotationTypeEnum;
+use App\Repository\Booking\CourseRepository;
+use App\Service\OnChange\Organization\OnParametersChange;
+use PHPUnit\Framework\TestCase;
+
+class OnParametersChangeTest extends TestCase
+{
+    private Parameters $parameters;
+    private OnParametersChange $onParametersChange;
+    private CourseRepository $courseRepositoryMock;
+
+    public function setUp():void
+    {
+        $this->courseRepositoryMock = $this->getMockBuilder(CourseRepository::class)->disableOriginalConstructor()->getMock();
+        $this->parameters = new Parameters();
+        $this->onParametersChange = new OnParametersChange($this->courseRepositoryMock);
+    }
+
+    /**
+     * @see OnParametersChange::onAdvancedEducationNotationTypeChange()
+     */
+    public function testOnAdvancedEducationNotationTypeByTeachersChange(){
+        $educationNotationConfig = new EducationNotationConfig();
+        $educationCurriculum = new EducationCurriculum();
+        $educationNotationConfig->addEducationCurriculum($educationCurriculum);
+
+        $organization = new Organization();
+        $organization->addEducationNotationConfig($educationNotationConfig);
+
+        $this->parameters->setAdvancedEducationNotationType(AdvancedEducationNotationTypeEnum::BY_TEACHER()->getValue());
+        $this->parameters->setOrganization($organization);
+
+        $this->assertCount(1, $educationNotationConfig->getEducationCurriculums());
+        $this->onParametersChange->onAdvancedEducationNotationTypeChange($this->parameters);
+        $this->assertNull($educationNotationConfig->getEducationCurriculums()->first()->getEducationNotationConfig());
+    }
+
+    /**
+     * @see OnParametersChange::onAdvancedEducationNotationTypeChange()
+     */
+    public function testOnAdvancedEducationNotationTypeByEducationChange(){
+        $educationNotationConfig = new EducationNotationConfig();
+        $teacher = new Access();
+        $educationNotationConfig->addTeacher($teacher);
+
+        $organization = new Organization();
+        $organization->addEducationNotationConfig($educationNotationConfig);
+
+        $this->parameters->setAdvancedEducationNotationType(AdvancedEducationNotationTypeEnum::BY_EDUCATION()->getValue());
+        $this->parameters->setOrganization($organization);
+
+        $this->assertCount(1, $educationNotationConfig->getTeachers());
+        $this->onParametersChange->onAdvancedEducationNotationTypeChange($this->parameters);
+        $this->assertNull($educationNotationConfig->getTeachers()->first()->getEducationNotationConfig());
+    }
+
+    /**
+     * Un cours qui débute le 2/09/2022, si l'année musical passe du 05/09 au 01/09 alors le cours passe de l'année 2021/2022 à 2022/2023
+     * @throws \Exception
+     * @see OnParametersChange::onMusicalDateChange()
+     */
+    public function testOnMusicalDateChange(){
+        $organization = new Organization();
+        $this->parameters->setMusicalDate(new \DateTime('2022-09-01'));
+        $organization->setParameters($this->parameters);
+
+
+        $course = new Course();
+        $course->setStartYear(2021);
+        $course->setEndYear(2022);
+        $course->setDatetimeStart(new \DateTime('2022-09-02'));
+
+        $this->courseRepositoryMock
+            ->method('getCoursesToFrom')
+            ->willReturn([$course])
+        ;
+
+        $this->onParametersChange->onMusicalDateChange($organization, new \DateTime('2022-09-05'));
+
+        $this->assertEquals(2022, $course->getStartYear());
+        $this->assertEquals(2023, $course->getEndYear());
+    }
+}

+ 25 - 0
tests/Service/Organization/UtilsTest.php

@@ -105,4 +105,29 @@ class UtilsTest extends TestCase
         else
             $this->assertEquals($today->format('Y'), Utils::getOrganizationCurrentActivityYear($this->organization));
     }
+
+    /**
+     * @see OrganizationUtils::getActivityPeriodsSwitchYear()
+     */
+    public function testGetActivityPeriodsSwitchYear(){
+        $parameters = new Parameters();
+        $parameters->setMusicalDate(new \DateTime('2020-09-05'));
+        $this->organization->setParameters($parameters);
+        $periods = Utils::getActivityPeriodsSwitchYear($this->organization, 2022);
+
+        $this->assertEquals('2022-09-05', $periods['dateStart']);
+        $this->assertEquals('2023-09-04', $periods['dateEnd']);
+    }
+
+    /**
+     * @see OrganizationUtils::getActivityYearSwitchDate()
+     */
+    public function testgetActivityYearSwitchDate(){
+        $parameters = new Parameters();
+        $parameters->setMusicalDate(new \DateTime('2020-09-05'));
+        $this->organization->setParameters($parameters);
+
+        $this->assertEquals(2022, Utils::getActivityYearSwitchDate($this->organization, new \DateTime('2022-09-10')));
+        $this->assertEquals(2021, Utils::getActivityYearSwitchDate($this->organization, new \DateTime('2022-09-02')));
+    }
 }

+ 0 - 21
tests/Service/ServiceIterator/CurrentAccessExtensionIteratorTest.php

@@ -31,25 +31,4 @@ class CurrentAccessExtensionIteratorTest extends TestCase
 
         $this->assertEquals(true, $actualExt);
     }
-
-    public function testAddWhereError() {
-        $queryBuilder = $this->getMockBuilder(QueryBuilder::class)
-            ->disableOriginalConstructor()
-            ->getMock();
-
-        $mocker = $this->getMockBuilder(AccessExtensionInterface::class);
-
-        $ext1 = $mocker->getMock();
-        $ext1->method('support')->willReturn(false);
-
-        $ext2 = $mocker->getMock();
-        $ext2->method('support')->willReturn(false);
-
-        $extensions = [$ext1, $ext2];
-
-        $iterator = new CurrentAccessExtensionIterator($extensions);
-
-        $this->expectException(Exception::class);
-        $iterator->addWhere($queryBuilder, 'foo');
-    }
 }