Преглед на файлове

Merge branch 'release/0.2'

Vincent GUFFON преди 3 години
родител
ревизия
993fe00fcd
променени са 86 файла, в които са добавени 2964 реда и са изтрити 300 реда
  1. 1 1
      .env
  2. 28 0
      .env.ci
  3. 1 0
      .gitignore
  4. 20 3
      .gitlab-ci.yml
  5. 2 0
      composer.json
  6. 302 196
      composer.lock
  7. 36 0
      config/api_platform/Access/access.yaml
  8. 2 0
      config/bundles.php
  9. 3 2
      config/opentalent/enum.yaml
  10. 4 1
      config/packages/api_platform.yaml
  11. 4 0
      config/packages/dev/debug.yaml
  12. 19 0
      config/packages/dev/monolog.yaml
  13. 4 0
      config/packages/doctrine.yaml
  14. 8 0
      config/packages/prod/deprecations.yaml
  15. 17 0
      config/packages/prod/monolog.yaml
  16. 12 0
      config/packages/test/monolog.yaml
  17. 16 2
      config/services.yaml
  18. 5 0
      phpunit.xml.dist
  19. 16 0
      src/Annotation/DateTimeConstraintAware.php
  20. 15 0
      src/Annotation/OrganizationDefaultValue.php
  21. 11 7
      src/Doctrine/Access/CurrentAccessExtension.php
  22. 2 2
      src/Doctrine/Access/CurrentUserPersonalizedListExtension.php
  23. 3 2
      src/Doctrine/Access/Extensions/AdminExtension.php
  24. 25 0
      src/Doctrine/Access/Extensions/DateTimeConstraintExtension.php
  25. 1 1
      src/Doctrine/Access/HandleCurrentAccessExtension.php
  26. 48 0
      src/Doctrine/Core/AllowedAddressPostalExtension.php
  27. 1 1
      src/Doctrine/Core/CurrentNotificationUserExtension.php
  28. 1 1
      src/Doctrine/Core/CurrentUserNotificationExtension.php
  29. 47 0
      src/Doctrine/Network/CurrentNetworkOrganizationExtension.php
  30. 47 0
      src/Doctrine/Organization/CurrentOrganizationAddressPostalExtension.php
  31. 47 0
      src/Doctrine/Organization/CurrentOrganizationArticleExtension.php
  32. 47 0
      src/Doctrine/Organization/CurrentOrganizationExtension.php
  33. 11 26
      src/Entity/Access/Access.php
  34. 3 0
      src/Entity/Access/OrganizationFunction.php
  35. 59 0
      src/Entity/Billing/BillingSetting.php
  36. 38 7
      src/Entity/Core/AddressPostal.php
  37. 11 3
      src/Entity/Core/BankAccount.php
  38. 14 3
      src/Entity/Core/ContactPoint.php
  39. 8 0
      src/Entity/Core/Country.php
  40. 85 0
      src/Entity/Core/File.php
  41. 9 0
      src/Entity/Network/Network.php
  42. 46 5
      src/Entity/Network/NetworkOrganization.php
  43. 126 2
      src/Entity/Organization/Organization.php
  44. 30 5
      src/Entity/Organization/OrganizationAddressPostal.php
  45. 95 0
      src/Entity/Organization/OrganizationArticle.php
  46. 4 1
      src/Entity/Organization/Parameters.php
  47. 113 0
      src/Entity/Organization/TypeOfPractice.php
  48. 58 2
      src/Entity/Person/Person.php
  49. 84 0
      src/Entity/Person/PersonAddressPostal.php
  50. 1 0
      src/Entity/Traits/ActivityPeriodTrait.php
  51. 0 9
      src/Enum/Core/ContactPointTypeEnum.php
  52. 18 0
      src/Enum/Cotisation/CategoryTypeOfPracticeEnum.php
  53. 51 0
      src/Enum/Cotisation/TypeOfPracticeEnum.php
  54. 1 1
      src/Enum/Organization/AddressPostalOrganizationTypeEnum.php
  55. 15 0
      src/Enum/Person/AddressPostalPersonTypeEnum.php
  56. 38 0
      src/EventListener/DoctrineFilter/DoctrineFilterListener.php
  57. 17 0
      src/EventListener/Helper.php
  58. 68 0
      src/EventListener/Organization/OrganizationChangedSubscriber.php
  59. 103 0
      src/Filter/DoctrineFilter/DateTimeFilter.php
  60. 41 0
      src/Filter/Person/FullNameFilter.php
  61. 7 1
      src/Repository/Access/AccessRepository.php
  62. 16 0
      src/Repository/Billing/BillingSettingRepository.php
  63. 38 0
      src/Repository/Core/ContactPointRepository.php
  64. 4 4
      src/Repository/Cotisation/CotisationApiResourcesRepository.php
  65. 18 0
      src/Repository/Organization/OrganizationAddressPostalRepository.php
  66. 18 0
      src/Repository/Organization/OrganizationArticleRepository.php
  67. 18 0
      src/Repository/Organization/TypeOfPracticeRepository.php
  68. 16 0
      src/Repository/Person/PersonAddressPostalRepository.php
  69. 2 1
      src/Security/Voter/BankAccountVoter.php
  70. 2 2
      src/Security/Voter/ContactPointVoter.php
  71. 63 0
      src/Serializer/DefaultNormalizer.php
  72. 29 0
      src/Service/Organization/Utils.php
  73. 236 0
      src/Service/Utils/DateTimeConstraint.php
  74. 33 0
      src/Service/Utils/EntityUtils.php
  75. 3 3
      src/Service/Utils/Reflection.php
  76. 23 0
      src/Service/Utils/StringsUtils.php
  77. 17 0
      src/Validator/Core/ContactPoint.php
  78. 42 0
      src/Validator/Core/ContactPointValidator.php
  79. 17 0
      src/Validator/Organization/OrganizationAddressPostal.php
  80. 37 0
      src/Validator/Organization/OrganizationAddressPostalValidator.php
  81. 36 3
      symfony.lock
  82. 76 0
      tests/Filter/DoctrineFilter/DateTimeFilterTest.php
  83. 8 3
      tests/Service/Cotisation/UtilsTest.php
  84. 220 0
      tests/Service/Utils/DateTimeConstraintTest.php
  85. 16 0
      tests/Service/Utils/StringsUtilsTest.php
  86. 27 0
      tests/TestToolsTrait.php

+ 1 - 1
.env

@@ -30,7 +30,7 @@ APP_SECRET=6a76497c8658bb23e2236f97a2627df3
 ###< doctrine/doctrine-bundle ###
 
 ###> nelmio/cors-bundle ###
-#CORS_ALLOW_ORIGIN=^https?://(localhost|127\.0\.0\.1)(:[0-9]+)|local.admin.opentalent.fr|local.admin2.opentalent.fr?$
+#CORS_ALLOW_ORIGIN=^https?://(localhost|127\.0\.0\.1)(:[0-9]+)|local.admin.opentalent.fr|local.app.opentalent.fr?$
 ###< nelmio/cors-bundle ###
 
 ###> lexik/jwt-authentication-bundle ###

+ 28 - 0
.env.ci

@@ -0,0 +1,28 @@
+###> symfony/framework-bundle ###
+APP_ENV=dev
+APP_DEBUG=1
+APP_SECRET=6a76497c8658bb23e2236f97a2627df3
+#TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
+#TRUSTED_HOSTS='^(localhost|example\.com)$'
+###< symfony/framework-bundle ###
+
+###> doctrine/doctrine-bundle ###
+# >>> No database shall be needed by unit tests
+DATABASE_URL=mysql://root:xxx@none:3306/opentalent?serverVersion=5.7
+###< doctrine/doctrine-bundle ###
+
+###> nelmio/cors-bundle ###
+CORS_ALLOW_ORIGIN=^https?://(localhost|127\.0\.0\.1)(:[0-9]+)$
+###< nelmio/cors-bundle ###
+
+###> BlackFire configuration ###
+BLACKFIRE_CLIENT_ID=988fcba8-552d-48df-a9c2-035c76535b69
+BLACKFIRE_CLIENT_TOKEN=8cfbeb263d044da9678dc2612531504da3790c308da7448e35724a5da91c136f
+BLACKFIRE_SERVER_ID=1171e53b-459b-41da-a292-80ff68cee8c2
+BLACKFIRE_SERVER_TOKEN=dbd1cfbea015fe83cccfc189a36ca3c16f3a1b43b94f50032a15e41e53548e8b
+###< BlackFire configuration ###
+
+###> AdminAssos configuration ###
+# >>> No database shall be needed by unit tests
+DATABASE_ADMINASSOS_URL=mysql://root:xxx@preprod:3306/none?serverVersion=5.7
+###< AdminAssos configuration ###

+ 1 - 0
.gitignore

@@ -34,3 +34,4 @@ symfony.lock
 /phpunit.xml
 .phpunit.result.cache
 ###< phpunit/phpunit ###
+/coverage/

+ 20 - 3
.gitlab-ci.yml

@@ -1,7 +1,24 @@
 stages:
   - test
 
+variables:
+  APP_ENV: ci
+  SSH_PRIVATE_KEY: $SSH_PRIVATE_KEY
+
 before_script:
+  - apt-get -yqq update
+  - apt-get -yqq install zip unzip git openssh-client
+
+  # Run ssh-agent and add private key
+  - eval $(ssh-agent -s)
+  - mkdir -p ~/.ssh
+  - chmod 700 ~/.ssh
+  - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
+  - ssh-add <(echo "$SSH_PRIVATE_KEY")
+  - git config --global user.email "exploitation@opentalent.fr"
+  - git config --global user.name "git"
+
+  # install composer
   - curl -sS https://composer.github.io/installer.sig > installer.sig
   - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
   - php -r "if (hash_file('SHA384', 'composer-setup.php') === file_get_contents('installer.sig')) { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
@@ -12,12 +29,12 @@ cache:
   paths:
     - ./vendor
 
-unit:otcore:
+unit:
   stage: test
 
   script:
-    - php composer.phar --quiet --no-interaction install
-    - ./bin/phpunit -c ./Tests/Build/UnitTests.xml --coverage-text --colors=never
+    - php composer.phar --no-interaction --quiet install
+    - ./bin/phpunit --configuration ./phpunit.xml.dist --coverage-text --colors=never
 
   artifacts:
     paths:

+ 2 - 0
composer.json

@@ -32,6 +32,7 @@
         "symfony/framework-bundle": "5.3.*",
         "symfony/http-client": "5.3.*",
         "symfony/intl": "5.3.*",
+        "symfony/monolog-bundle": "^3.0",
         "symfony/property-access": "5.3.*",
         "symfony/property-info": "5.3.*",
         "symfony/security-bundle": "5.3.*",
@@ -44,6 +45,7 @@
     },
     "require-dev": {
         "cyclonedx/cyclonedx-php-composer": "^3.4",
+        "symfony/debug-bundle": "5.3.*",
         "symfony/maker-bundle": "^1.21",
         "symfony/phpunit-bridge": "^5.3",
         "symfony/stopwatch": "^5.3",

Файловите разлики са ограничени, защото са твърде много
+ 302 - 196
composer.lock


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

@@ -0,0 +1,36 @@
+App\Entity\Access\Access:
+  collectionOperations:
+    get: ~
+
+    cget_students:
+      method: GET
+      path: '/students'
+      security: 'is_granted("ROLE_USERS_VIEW")'
+
+    cget_admin:
+      method: GET
+      path: '/admin'
+
+    cget_access_person_ref:
+      method: GET
+      path: '/access_people'
+      normalization_context:
+        groups: ['access_people_ref']
+
+  itemOperations:
+    get:
+      security: '(is_granted("ROLE_USERS_VIEW") and object.getOrganization().getId() == user.getOrganization().getId()) or (object.getId() == user.getId())'
+
+    get_access_address:
+      method: GET
+      path: '/access_addresses/{id}'
+      requirements:
+        id : '\d+'
+      normalization_context:
+        groups: ['access_address', 'address']]
+      security: 'object.getOrganization().getId() == user.getOrganization().getId()'
+
+    put:
+      security: 'is_granted("ROLE_USERS") or (object.getId() == user.getId())'
+
+    delete: ~

+ 2 - 0
config/bundles.php

@@ -14,4 +14,6 @@ return [
     Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
     Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
     FOS\ElasticaBundle\FOSElasticaBundle::class => ['all' => true],
+    Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
+    Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
 ];

+ 3 - 2
config/opentalent/enum.yaml

@@ -20,8 +20,6 @@ opentalent:
     family_situation: 'App\Enum\AccessSocial\FamilySituationEnum'
 
   #Core
-    address_postal_person: 'App\Enum\Core\AddressPostalPersonTypeEnum'
-    address_postal_organization: 'App\Enum\Organization\AddressPostalOrganizationTypeEnum'
     period: 'App\Enum\Core\PeriodEnum'
     action_status_type: 'App\Enum\Core\ActionStatusTypeEnum'
     control_type: 'App\Enum\Core\ControlTypeEnum'
@@ -65,6 +63,7 @@ opentalent:
     organization_bulletin_output: 'App\Enum\Organization\BulletinOutputEnum'
     organization_bulletin_send_to: 'App\Enum\Organization\SendToBulletinEnum'
     organization_setting_country: 'App\Enum\Organization\CountryEnum'
+    address_postal_organization: 'App\Enum\Organization\AddressPostalOrganizationTypeEnum'
 
   #Person
     person_gender: 'App\Enum\Person\GenderEnum'
@@ -72,6 +71,7 @@ opentalent:
     person_medal_type: 'App\Enum\Person\MedalTypeEnum'
     person_moral_type: 'App\Enum\Person\MoralPersonTypeEnum'
     medal_type: 'App\Enum\Person\MedalTypeEnum'
+    address_postal_person: 'App\Enum\Person\AddressPostalPersonTypeEnum'
 
   #Place
     place_day_of_week: 'App\Enum\Place\DayOfWeekEnum'
@@ -166,6 +166,7 @@ opentalent:
   #Cotisation
     cotisation_function_enum_choices: 'App\Enum\Cotisation\CotisationFunctionEnum'
     type_of_practices_enum: 'App\Enum\Cotisation\TypeOfPracticeEnum'
+    category_type_of_practices_enum: 'App\Enum\Cotisation\CategoryTypeOfPracticeEnum'
 
   #OnlineRegistration
     onlineregistration_registration_status: 'App\Enum\OnlineRegistration\RegistrationStatus'

+ 4 - 1
config/packages/api_platform.yaml

@@ -2,7 +2,10 @@ api_platform:
     enable_swagger_ui: false
     enable_re_doc: false
     mapping:
-        paths: ['%kernel.project_dir%/src/Entity', '%kernel.project_dir%/src/ApiResources']
+        paths:
+            - '%kernel.project_dir%/src/Entity'
+            - '%kernel.project_dir%/src/ApiResources'
+            - '%kernel.project_dir%/config/api_platform'
     patch_formats:
         json: ['application/merge-patch+json']
     swagger:

+ 4 - 0
config/packages/dev/debug.yaml

@@ -0,0 +1,4 @@
+debug:
+    # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
+    # See the "server:dump" command to start a new server.
+    dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"

+ 19 - 0
config/packages/dev/monolog.yaml

@@ -0,0 +1,19 @@
+monolog:
+    handlers:
+        main:
+            type: stream
+            path: "%kernel.logs_dir%/%kernel.environment%.log"
+            level: debug
+            channels: ["!event"]
+        # uncomment to get logging in your browser
+        # you may have to allow bigger header sizes in your Web server configuration
+        #firephp:
+        #    type: firephp
+        #    level: info
+        #chromephp:
+        #    type: chromephp
+        #    level: info
+        console:
+            type: console
+            process_psr_3_messages: false
+            channels: ["!event", "!doctrine", "!console"]

+ 4 - 0
config/packages/doctrine.yaml

@@ -21,6 +21,10 @@ doctrine:
         auto_generate_proxy_classes: true
         entity_managers:
             default:
+                filters:
+                    date_time_filter:
+                        class: App\Filter\DoctrineFilter\DateTimeFilter
+                        enabled: true
                 connection: default
                 auto_mapping: true
                 mappings:

+ 8 - 0
config/packages/prod/deprecations.yaml

@@ -0,0 +1,8 @@
+# As of Symfony 5.1, deprecations are logged in the dedicated "deprecation" channel when it exists
+#monolog:
+#    channels: [deprecation]
+#    handlers:
+#        deprecation:
+#            type: stream
+#            channels: [deprecation]
+#            path: php://stderr

+ 17 - 0
config/packages/prod/monolog.yaml

@@ -0,0 +1,17 @@
+monolog:
+    handlers:
+        main:
+            type: fingers_crossed
+            action_level: error
+            handler: nested
+            excluded_http_codes: [404, 405]
+            buffer_size: 50 # How many messages should be saved? Prevent memory leaks
+        nested:
+            type: stream
+            path: php://stderr
+            level: debug
+            formatter: monolog.formatter.json
+        console:
+            type: console
+            process_psr_3_messages: false
+            channels: ["!event", "!doctrine"]

+ 12 - 0
config/packages/test/monolog.yaml

@@ -0,0 +1,12 @@
+monolog:
+    handlers:
+        main:
+            type: fingers_crossed
+            action_level: error
+            handler: nested
+            excluded_http_codes: [404, 405]
+            channels: ["!event"]
+        nested:
+            type: stream
+            path: "%kernel.logs_dir%/%kernel.environment%.log"
+            level: debug

+ 16 - 2
config/services.yaml

@@ -37,14 +37,28 @@ services:
         App\Service\Access\OptionalsRolesInterface:
             tags: ['app.optionalsroles']
 
-    App\Doctrine\Access\HandleAccessExtension:
+    App\Doctrine\Access\HandleCurrentAccessExtension:
         - !tagged_iterator app.extensions.access
     App\Service\Access\HandleOptionalsRoles:
         - !tagged_iterator app.optionalsroles
 
     #########################################
     ##  SERIALIZER Decorates ##
+    App\Serializer\DefaultNormalizer:
+        # By default .inner is passed as argument
+        decorates: 'api_platform.jsonld.normalizer.item'
+
+    app.serializer.normalizer.item.json:
+        class: 'App\Serializer\DefaultNormalizer'
+        decorates: 'api_platform.serializer.normalizer.item'
+
     App\Serializer\AccessContextBuilder:
         decorates: 'api_platform.serializer.context_builder'
         arguments: [ '@App\Serializer\AccessContextBuilder.inner' ]
-        autoconfigure: false
+        autoconfigure: false
+
+    #########################################
+    ##  LISTENER ##
+    App\EventListener\DoctrineFilter\DoctrineFilterListener:
+        tags:
+            - { name: kernel.event_listener, event: kernel.request }

+ 5 - 0
phpunit.xml.dist

@@ -30,4 +30,9 @@
     <listeners>
         <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
     </listeners>
+
+    <logging>
+        <log type="coverage-html" target="./coverage" lowUpperBound="35" highLowerBound="70"/>
+        <log type="junit" target="./coverage/junit-report.xml"/>
+    </logging>
 </phpunit>

+ 16 - 0
src/Annotation/DateTimeConstraintAware.php

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

+ 15 - 0
src/Annotation/OrganizationDefaultValue.php

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

+ 11 - 7
src/Doctrine/Access/AccessExtension.php → src/Doctrine/Access/CurrentAccessExtension.php

@@ -11,12 +11,15 @@ use Doctrine\ORM\QueryBuilder;
 use Symfony\Component\Security\Core\Security;
 
 /**
- * Class AccessExtension : Filtre de sécurité par défaut pour une resource Access
+ * Class CurrentAccessExtension : Filtre de sécurité par défaut pour une resource Access
  * @package App\Doctrine\Access
  */
-final class AccessExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentAccessExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
 {
-    public function __construct(private Security $security, private HandleAccessExtension $handleAccessExtension)
+    public function __construct(
+        private Security $security,
+        private HandleCurrentAccessExtension $handleCurrentAccessExtension
+    )
     { }
 
     public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
@@ -34,13 +37,14 @@ final class AccessExtension implements QueryCollectionExtensionInterface, QueryI
         if (Access::class !== $resourceClass) {
             return;
         }
-
         /** @var Access $currentUser */
         $currentUser = $this->security->getUser();
         $rootAlias = $queryBuilder->getRootAliases()[0];
-        $queryBuilder->andWhere(sprintf('%s.organization = :current_organization', $rootAlias));
-        $queryBuilder->setParameter('current_organization', $currentUser->getOrganization());
+        $queryBuilder
+            ->andWhere(sprintf('%s.organization = :current_organization', $rootAlias))
+            ->setParameter('current_organization', $currentUser->getOrganization())
+        ;
 
-        $this->handleAccessExtension->addWhere($queryBuilder, $operationName);
+        $this->handleCurrentAccessExtension->addWhere($queryBuilder, $operationName);
     }
 }

+ 2 - 2
src/Doctrine/Access/PersonalizedListExtension.php → src/Doctrine/Access/CurrentUserPersonalizedListExtension.php

@@ -11,10 +11,10 @@ use Doctrine\ORM\QueryBuilder;
 use Symfony\Component\Security\Core\Security;
 
 /**
- * Class PersonalizedListExtension : Filtre de sécurité par défaut pour une resource PersonalizedList
+ * Class CurrentUserPersonalizedListExtension : Filtre de sécurité par défaut pour une resource PersonalizedList
  * @package App\Doctrine\Access
  */
-final class PersonalizedListExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentUserPersonalizedListExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
 {
     public function __construct(private Security $security)
     { }

+ 3 - 2
src/Doctrine/Access/Extensions/AdminExtension.php

@@ -15,7 +15,8 @@ class AdminExtension implements AccessExtensionInterface {
     public function addWhere(QueryBuilder $queryBuilder)
     {
         $rootAlias = $queryBuilder->getRootAliases()[0];
-        $queryBuilder->andWhere(sprintf('%s.adminAccess = :adminAccess', $rootAlias));
-        $queryBuilder->setParameter('adminAccess', true);
+        $queryBuilder
+            ->andWhere(sprintf('%s.adminAccess = :adminAccess', $rootAlias))
+            ->setParameter('adminAccess', true);
     }
 }

+ 25 - 0
src/Doctrine/Access/Extensions/DateTimeConstraintExtension.php

@@ -0,0 +1,25 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Doctrine\Access\Extensions;
+
+use App\Doctrine\Access\AccessExtensionInterface;
+use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+class DateTimeConstraintExtension implements AccessExtensionInterface {
+    public function __construct(
+        private RequestStack $requestStack
+    ){
+    }
+    public function support(string $name): bool
+    {
+        return $this->requestStack->getMainRequest()->get('_time_constraint', true) == true;
+    }
+
+    public function addWhere(QueryBuilder $queryBuilder)
+    {
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder->innerJoin(sprintf('%s.organizationFunction', $rootAlias), 'organization_function');
+    }
+}

+ 1 - 1
src/Doctrine/Access/HandleAccessExtension.php → src/Doctrine/Access/HandleCurrentAccessExtension.php

@@ -5,7 +5,7 @@ namespace App\Doctrine\Access;
 
 use Doctrine\ORM\QueryBuilder;
 
-class HandleAccessExtension{
+class HandleCurrentAccessExtension{
     public function __construct(private iterable $extensions)
     { }
 

+ 48 - 0
src/Doctrine/Core/AllowedAddressPostalExtension.php

@@ -0,0 +1,48 @@
+<?php
+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\Entity\Access\Access;
+use App\Entity\Core\AddressPostal;
+use Doctrine\ORM\QueryBuilder;
+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
+{
+    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
+    {
+        if (AddressPostal::class !== $resourceClass) {
+            return;
+        }
+
+        /** @var Access $currentUser */
+        $currentUser = $this->security->getUser();
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder
+            ->innerJoin(sprintf('%s.organizationAddressPostal', $rootAlias), 'organization_address_postal')
+            ->andWhere('organization_address_postal.organization = :organization')
+            ->setParameter('organization', $currentUser->getOrganization())
+        ;
+    }
+}

+ 1 - 1
src/Doctrine/Core/NotificationUserExtension.php → src/Doctrine/Core/CurrentNotificationUserExtension.php

@@ -15,7 +15,7 @@ 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 NotificationUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentNotificationUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
 {
     public function __construct(private Security $security)
     { }

+ 1 - 1
src/Doctrine/Core/NotificationExtension.php → src/Doctrine/Core/CurrentUserNotificationExtension.php

@@ -15,7 +15,7 @@ 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 NotificationExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
+final class CurrentUserNotificationExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
 {
     public function __construct(private Security $security)
     { }

+ 47 - 0
src/Doctrine/Network/CurrentNetworkOrganizationExtension.php

@@ -0,0 +1,47 @@
+<?php
+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\Entity\Access\Access;
+use App\Entity\Network\NetworkOrganization;
+use Doctrine\ORM\QueryBuilder;
+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
+{
+    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
+    {
+        if (NetworkOrganization::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())
+        ;
+    }
+}

+ 47 - 0
src/Doctrine/Organization/CurrentOrganizationAddressPostalExtension.php

@@ -0,0 +1,47 @@
+<?php
+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\Entity\Access\Access;
+use App\Entity\Organization\OrganizationAddressPostal;
+use Doctrine\ORM\QueryBuilder;
+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
+{
+    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
+    {
+        if (OrganizationAddressPostal::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())
+        ;
+    }
+}

+ 47 - 0
src/Doctrine/Organization/CurrentOrganizationArticleExtension.php

@@ -0,0 +1,47 @@
+<?php
+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\Entity\Access\Access;
+use App\Entity\Organization\OrganizationArticle;
+use Doctrine\ORM\QueryBuilder;
+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
+{
+    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
+    {
+        if (OrganizationArticle::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())
+        ;
+    }
+}

+ 47 - 0
src/Doctrine/Organization/CurrentOrganizationExtension.php

@@ -0,0 +1,47 @@
+<?php
+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\Entity\Access\Access;
+use App\Entity\Organization\Organization;
+use Doctrine\ORM\QueryBuilder;
+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
+{
+    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
+    {
+        if (Organization::class !== $resourceClass) {
+            return;
+        }
+
+        /** @var Access $currentUser */
+        $currentUser = $this->security->getUser();
+        $rootAlias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder
+            ->andWhere(sprintf('%s.id = :organization', $rootAlias))
+            ->setParameter('organization', $currentUser->getOrganization()->getId())
+        ;
+    }
+}

+ 11 - 26
src/Entity/Access/Access.php

@@ -3,6 +3,9 @@ declare(strict_types=1);
 
 namespace App\Entity\Access;
 
+use ApiPlatform\Core\Annotation\ApiFilter;
+use App\Filter\Person\FullNameFilter;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\BooleanFilter;
 use ApiPlatform\Core\Annotation\ApiResource;
 use ApiPlatform\Core\Annotation\ApiSubresource;
 use App\Entity\Billing\AccessIntangible;
@@ -23,36 +26,17 @@ use Symfony\Component\Serializer\Annotation\Groups;
 
 /**
  * Fais le lien entre une Person et une Organization
+ * @ApiResource @see : config/api_platform/Access/access.yaml
  */
-#[ApiResource(
-    collectionOperations:[
-        'cget_students'=> [
-            'method' => 'GET',
-            'path' => '/students',
-            'security' => 'is_granted("ROLE_USERS_VIEW")'
-        ],
-        'cget_admin'=> [
-            'method' => 'GET',
-            'path' => '/admin'
-        ],
-        'get'
-    ],
-    itemOperations: [
-        'get' => [
-            'security' => '(is_granted("ROLE_USERS_VIEW") and object.getOrganization().getId() == user.getOrganization().getId()) or (object.getId() == user.getId())'
-        ],
-        'put' => [
-            'security' => 'is_granted("ROLE_USERS") or (object.getId() == user.getId())'
-        ],
-        'delete'
-    ]
-)]
 #[ORM\Entity(repositoryClass: AccessRepository::class)]
+#[ApiFilter(BooleanFilter::class, properties: ['person.isPhysical'])]
+#[ApiFilter(FullNameFilter::class)]
 class Access implements UserInterface
 {
     #[ORM\Id]
     #[ORM\Column]
     #[ORM\GeneratedValue]
+    #[Groups("access_people_ref")]
     private ?int $id = null;
 
     #[ORM\Column(options: ['default' => false])]
@@ -68,6 +52,7 @@ class Access implements UserInterface
 
     #[ORM\ManyToOne(cascade: ['persist'])]
     #[ORM\JoinColumn(nullable: false)]
+    #[Groups(["access_people_ref", "access_address"])]
     private Person $person;
 
     #[ORM\ManyToOne]
@@ -520,17 +505,17 @@ class Access implements UserInterface
         return $this->person->getUsername();
     }
 
-    public function getPassword()
+    public function getPassword(): ?string
     {
         // TODO: Implement getPassword() method.
     }
 
-    public function getSalt()
+    public function getSalt(): ?string
     {
         // TODO: Implement getSalt() method.
     }
 
-    public function getUsername()
+    public function getUsername(): ?string
     {
         // TODO: Implement getUsername() method.
     }

+ 3 - 0
src/Entity/Access/OrganizationFunction.php

@@ -4,6 +4,7 @@ declare(strict_types=1);
 namespace App\Entity\Access;
 
 use ApiPlatform\Core\Annotation\ApiResource;
+use App\Annotation\DateTimeConstraintAware;
 use App\Entity\Traits\ActivityPeriodTrait;
 use App\Repository\Access\OrganizationFunctionRepository;
 use Doctrine\ORM\Mapping as ORM;
@@ -13,7 +14,9 @@ use Symfony\Component\Validator\Constraints as Assert;
  * Fonction d'un Access dans une Organization sur une période donnée
  */
 #[ApiResource]
+
 #[ORM\Entity(repositoryClass: OrganizationFunctionRepository::class)]
+#[DateTimeConstraintAware(startDateFieldName: "startDate", endDateFieldName: "endDate")]
 class OrganizationFunction
 {
     use ActivityPeriodTrait;

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

@@ -0,0 +1,59 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Entity\Billing;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use App\Entity\Organization\Organization;
+use App\Repository\Billing\BillingSettingRepository;
+use Doctrine\ORM\Mapping as ORM;
+
+#[ApiResource(
+    collectionOperations: [],
+    itemOperations: ['get']
+)]
+#[ORM\Entity(repositoryClass: BillingSettingRepository::class)]
+class BillingSetting
+{
+    #[ORM\Id]
+    #[ORM\Column]
+    #[ORM\GeneratedValue]
+    private ?int $id = null;
+
+    #[ORM\OneToOne(inversedBy: 'billingSetting')]
+    #[ORM\JoinColumn(nullable: false)]
+    private Organization $organization;
+
+    #[ORM\Column(options: ['default' => false])]
+    private bool $applyVat = false;
+
+    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 getApplyVat(): bool
+    {
+        return $this->applyVat;
+    }
+
+    public function setApplyVat(bool $applyVat): self
+    {
+        $this->applyVat = $applyVat;
+
+        return $this;
+    }
+}

+ 38 - 7
src/Entity/Core/AddressPostal.php

@@ -3,48 +3,72 @@ declare(strict_types=1);
 
 namespace App\Entity\Core;
 
+use ApiPlatform\Core\Annotation\ApiResource;
 use App\Entity\Organization\OrganizationAddressPostal;
 use App\Repository\Core\AddressPostalRepository;
 use Doctrine\ORM\Mapping as ORM;
-
+use App\Entity\Person\PersonAddressPostal;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[ApiResource(
+    collectionOperations: [
+        "get"
+    ],
+    itemOperations: [
+        "get" => ["security" => "is_granted('ROLE_ORGANIZATION_VIEW') and object.getOrganizationAddressPostal().getOrganization().getId() == user.getOrganization().getId()"],
+    ]
+)]
 #[ORM\Entity(repositoryClass: AddressPostalRepository::class)]
 class AddressPostal
 {
     #[ORM\Id]
     #[ORM\Column]
     #[ORM\GeneratedValue]
+    #[Groups(["address", "access_address"])]
     private ?int $id = null;
 
     #[ORM\ManyToOne]
+    #[Groups("address")]
     private ?Country $addressCountry = null;
 
     #[ORM\Column(length: 100, nullable: true)]
+    #[Groups("address")]
     private ?string $addressCity = null;
 
     #[ORM\Column(length: 100, nullable: true)]
+    #[Groups("address")]
     private ?string $addressOwner = null;
 
     #[ORM\Column(length: 20, nullable: true)]
+    #[Groups("address")]
     private ?string $postalCode = null;
 
     #[ORM\Column(length: 255, nullable: true)]
+    #[Groups("address")]
     private ?string $streetAddress = null;
 
     #[ORM\Column(length: 255, nullable: true)]
+    #[Groups("address")]
     private ?string $streetAddressSecond = null;
 
     #[ORM\Column(length: 255, nullable: true)]
+    #[Groups("address")]
     private ?string $streetAddressThird = null;
 
     #[ORM\Column(nullable: true)]
+    #[Groups("address")]
     private ?float $latitude = null;
 
     #[ORM\Column(nullable: true)]
+    #[Groups("address")]
     private ?float $longitude = null;
 
-    #[ORM\OneToOne(mappedBy: 'addressPostal', cascade: ['persist', 'remove'])]
+    #[ORM\OneToOne(mappedBy: 'addressPostal')]
     private OrganizationAddressPostal $organizationAddressPostal;
 
+    #[ORM\OneToOne(mappedBy: 'addressPostal')]
+    private PersonAddressPostal $personAddressPostal;
+
     public function getId(): ?int
     {
         return $this->id;
@@ -165,13 +189,20 @@ class AddressPostal
 
     public function setOrganizationAddressPostal(OrganizationAddressPostal $organizationAddressPostal): self
     {
-        // set the owning side of the relation if necessary
-        if ($organizationAddressPostal->getAddressPostal() !== $this) {
-            $organizationAddressPostal->setAddressPostal($this);
-        }
-
         $this->organizationAddressPostal = $organizationAddressPostal;
 
         return $this;
     }
+
+    public function getPersonAddressPostal(): ?PersonAddressPostal
+    {
+        return $this->personAddressPostal;
+    }
+
+    public function setPersonAddressPostal(PersonAddressPostal $personAddressPostal): self
+    {
+        $this->personAddressPostal = $personAddressPostal;
+
+        return $this;
+    }
 }

+ 11 - 3
src/Entity/Core/BankAccount.php

@@ -22,6 +22,9 @@ use Symfony\Component\Validator\Constraints as Assert;
         ],
         'put' => [
             'security' => 'is_granted("BANK_ACCOUNT_EDIT", object)'
+        ],
+        'delete' => [
+            'security' => 'is_granted("BANK_ACCOUNT_DELETE", object)'
         ]
     ]
 )]
@@ -57,7 +60,7 @@ class BankAccount
      * 0 => jamais facturé, 1 => facturé 1 fois, 2 => facturé plusieurs fois
      */
     #[ORM\Column(options: ['default'=>0])]
-    private int $countInvoiced;
+    private ?int $countInvoiced = 0;
 
     #[ORM\Column(length: 255, nullable: true)]
     private ?string $holder = null;
@@ -73,10 +76,12 @@ class BankAccount
     private ?string $rum = null;
 
     #[ORM\Column(type: 'date', nullable: true)]
-    private \DateTimeInterface $signatureDateSamplingMandate;
+    private ?\DateTimeInterface $signatureDateSamplingMandate;
 
     #[ORM\ManyToMany(targetEntity: Organization::class, inversedBy: 'bankAccounts')]
     #[ORM\JoinTable(name: 'organization_bankaccount')]
+    #[ORM\JoinColumn(name: 'bankAccount_id', referencedColumnName: 'id', unique: true)]
+    #[ORM\InverseJoinColumn(name: 'organization_id', referencedColumnName: 'id')]
     private Collection $organization;
 
     #[Pure] public function __construct()
@@ -218,6 +223,7 @@ class BankAccount
     {
         if (!$this->organization->contains($organization)) {
             $this->organization[] = $organization;
+            $organization->addBankAccount($this);
         }
 
         return $this;
@@ -225,7 +231,9 @@ class BankAccount
 
     public function removeOrganization(Organization $organization): self
     {
-        $this->organization->removeElement($organization);
+        if ($this->organization->removeElement($organization)) {
+            $organization->removeBankAccount($this);
+        }
 
         return $this;
     }

+ 14 - 3
src/Entity/Core/ContactPoint.php

@@ -13,6 +13,7 @@ use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Component\Validator\Constraints as Assert;
+use App\Validator\Core as OpentalentAssert;
 
 /**
  * Données de contact d'une Person ou d'une Organization ou d'un lieu
@@ -31,6 +32,7 @@ use Symfony\Component\Validator\Constraints as Assert;
     ]
 )]
 #[ORM\Entity(repositoryClass: ContactPointRepository::class)]
+#[OpentalentAssert\ContactPoint]
 class ContactPoint
 {
     #[ORM\Id]
@@ -44,7 +46,6 @@ class ContactPoint
 
     #[ORM\Column(length: 255, nullable: true)]
     #[Assert\Email(message: 'invalid-email-format', mode: 'strict')]
-    #[Assert\Regex(pattern: '/^[a-zA-Z0-9._%-]{1,64}@[a-zA-Z0-9.-]{2,249}\.[a-zA-Z]{2,6}$/', message: 'email-error')]
     private ?string $email = null;
 
     #[ORM\Column(length: 255, nullable: true)]
@@ -70,10 +71,14 @@ class ContactPoint
 
     #[ORM\ManyToMany(targetEntity: Organization::class, inversedBy: 'contactPoints')]
     #[ORM\JoinTable(name: 'organization_contactpoint')]
+    #[ORM\JoinColumn(name: 'contactPoint_id', referencedColumnName: 'id', unique: true)]
+    #[ORM\InverseJoinColumn(name: 'organization_id', referencedColumnName: 'id')]
     private Collection $organization;
 
     #[ORM\ManyToMany(targetEntity: Person::class ,inversedBy: 'contactPoints')]
     #[ORM\JoinTable(name: 'person_contactpoint')]
+    #[ORM\JoinColumn(name: 'contactPoint_id', referencedColumnName: 'id', unique: true)]
+    #[ORM\InverseJoinColumn(name: 'person_id', referencedColumnName: 'id')]
     private Collection $person;
 
     #[Pure] public function __construct()
@@ -216,6 +221,7 @@ class ContactPoint
     {
         if (!$this->organization->contains($organization)) {
             $this->organization[] = $organization;
+            $organization->addContactPoint($this);
         }
 
         return $this;
@@ -223,7 +229,9 @@ class ContactPoint
 
     public function removeOrganization(Organization $organization): self
     {
-        $this->organization->removeElement($organization);
+        if ($this->organization->removeElement($organization)) {
+            $organization->removeContactPoint($this);
+        }
 
         return $this;
     }
@@ -237,6 +245,7 @@ class ContactPoint
     {
         if (!$this->person->contains($person)) {
             $this->person[] = $person;
+            $person->addContactPoint($this);
         }
 
         return $this;
@@ -244,7 +253,9 @@ class ContactPoint
 
     public function removePerson(Person $person): self
     {
-        $this->person->removeElement($person);
+        if ($this->person->removeElement($person)) {
+            $person->removeContactPoint($this);
+        }
 
         return $this;
     }

+ 8 - 0
src/Entity/Core/Country.php

@@ -3,9 +3,17 @@ declare(strict_types=1);
 
 namespace App\Entity\Core;
 
+use ApiPlatform\Core\Annotation\ApiResource;
 use App\Repository\Core\CountryRepository;
 use Doctrine\ORM\Mapping as ORM;
 
+#[ApiResource(
+    collectionOperations: ['get'],
+    itemOperations: ['get'],
+    attributes:[
+        'pagination_enabled' => false
+    ]
+)]
 #[ORM\Entity(repositoryClass: CountryRepository::class)]
 class Country
 {

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

@@ -3,6 +3,8 @@ declare(strict_types=1);
 
 namespace App\Entity\Core;
 
+use ApiPlatform\Core\Annotation\ApiResource;
+use App\Entity\Organization\Organization;
 use App\Entity\Person\Person;
 use App\Repository\Core\FileRepository;
 use Doctrine\Common\Collections\ArrayCollection;
@@ -10,6 +12,13 @@ use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
 use JetBrains\PhpStorm\Pure;
 
+#[ApiResource(
+    collectionOperations: [],
+    itemOperations: [
+        'get',
+        'put'
+    ]
+)]
 #[ORM\Entity(repositoryClass: FileRepository::class)]
 class File
 {
@@ -30,12 +39,23 @@ class File
     #[ORM\Column(length: 255, nullable: true)]
     private ?string $mimeType = null;
 
+    #[ORM\Column(length: 255)]
+    private string $config;
+
     #[ORM\OneToMany(mappedBy: 'image', targetEntity: Person::class, orphanRemoval: true)]
     private Collection $personImages;
 
+    #[ORM\OneToMany(mappedBy: 'logo', targetEntity: Organization::class, orphanRemoval: true)]
+    private Collection $organizationLogos;
+
+    #[ORM\OneToMany(mappedBy: 'image', targetEntity: Organization::class, orphanRemoval: true)]
+    private Collection $organizationImages;
+
     #[Pure] public function __construct()
     {
         $this->personImages = new ArrayCollection();
+        $this->organizationLogos = new ArrayCollection();
+        $this->organizationImages = new ArrayCollection();
     }
 
     public function getId(): ?int
@@ -88,6 +108,17 @@ class File
         return $this;
     }
 
+    public function getConfig(): string
+    {
+        return $this->config;
+    }
+
+    public function setConfig(string $config): self
+    {
+        $this->config = $config;
+        return $this;
+    }
+
     public function getPersonImages(): Collection
     {
         return $this->personImages;
@@ -114,4 +145,58 @@ class File
 
         return $this;
     }
+
+    public function getOrganizationLogos(): Collection
+    {
+        return $this->organizationLogos;
+    }
+
+    public function addOrganizationLogo(Organization $organization): self
+    {
+        if (!$this->organizationLogos->contains($organization)) {
+            $this->organizationLogos[] = $organization;
+            $organization->setLogo($this);
+        }
+
+        return $this;
+    }
+
+    public function removeOrganizationLogo(Organization $organization): self
+    {
+        if ($this->organizationLogos->removeElement($organization)) {
+            // set the owning side to null (unless already changed)
+            if ($organization->getLogo() === $this) {
+                $organization->setLogo(null);
+            }
+        }
+
+        return $this;
+    }
+
+    public function getOrganizationImages(): Collection
+    {
+        return $this->organizationImages;
+    }
+
+    public function addOrganizationImage(Organization $organization): self
+    {
+        if (!$this->organizationImages->contains($organization)) {
+            $this->organizationImages[] = $organization;
+            $organization->setImage($this);
+        }
+
+        return $this;
+    }
+
+    public function removeOrganizationImage(Organization $organization): self
+    {
+        if ($this->organizationImages->removeElement($organization)) {
+            // set the owning side to null (unless already changed)
+            if ($organization->getImage() === $this) {
+                $organization->setImage(null);
+            }
+        }
+
+        return $this;
+    }
 }

+ 9 - 0
src/Entity/Network/Network.php

@@ -3,21 +3,30 @@ declare(strict_types=1);
 
 namespace App\Entity\Network;
 
+use ApiPlatform\Core\Annotation\ApiResource;
 use App\Repository\Network\NetworkRepository;
 use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
 
 /**
  * Enum des différents réseaux auxquels peut appartenir une Organization
  */
+#[ApiResource(
+    collectionOperations: ["get"],
+    itemOperations: ['get'],
+    attributes: ["security" => "is_granted('ROLE_ORGANIZATION_VIEW')"]
+)]
 #[ORM\Entity(repositoryClass: NetworkRepository::class)]
 class Network
 {
     #[ORM\Id]
     #[ORM\Column]
     #[ORM\GeneratedValue]
+    #[Groups("network")]
     private ?int $id = null;
 
     #[ORM\Column(length: 255)]
+    #[Groups("network")]
     private string $name;
 
     #[ORM\Column(length: 255, nullable: true)]

+ 46 - 5
src/Entity/Network/NetworkOrganization.php

@@ -4,27 +4,42 @@ declare(strict_types=1);
 namespace App\Entity\Network;
 
 use ApiPlatform\Core\Annotation\ApiResource;
+use App\Annotation\DateTimeConstraintAware;
 use App\Entity\Organization\Organization;
 use App\Repository\Network\NetworkOrganizationRepository;
-use App\Entity\Traits\ActivityPeriodTrait;
 use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
 
 /**
  * Fait le lien entre une Organization et un Network
  */
-#[ApiResource]
+#[ApiResource(
+    collectionOperations: [
+        "get" => ["security" => "is_granted('ROLE_ORGANIZATION_VIEW')"]
+    ],
+    itemOperations: [
+        'get' => [
+            'security' => 'is_granted("ROLE_ORGANIZATION_VIEW" and object.getOrganization().getId() == user.getOrganization().getId()'
+        ]
+    ],
+    attributes: ["security" => "is_granted('ROLE_ORGANIZATION')"],
+    normalizationContext: [
+        'groups' => ['network'],
+    ]
+)]
 #[ORM\Entity(repositoryClass: NetworkOrganizationRepository::class)]
+#[DateTimeConstraintAware(startDateFieldName: "startDate", endDateFieldName: "endDate")]
 class NetworkOrganization
 {
-    use ActivityPeriodTrait;
-
     #[ORM\Id]
     #[ORM\Column]
     #[ORM\GeneratedValue]
+    #[Groups("network")]
     private ?int $id = null;
 
     #[ORM\ManyToOne]
     #[ORM\JoinColumn(nullable: true)]
+    #[Groups("network")]
     private Network $network;
 
     #[ORM\ManyToOne(inversedBy: 'networkOrganizations')]
@@ -32,11 +47,18 @@ class NetworkOrganization
     private Organization $organization;
 
     #[ORM\ManyToOne(inversedBy: 'networkOrganizationChildren')]
-    private Organization $parent;
+    private ?Organization $parent;
 
     #[ORM\Column(length: 255, nullable: true)]
     private ?string $leadingCause = null;
 
+    #[ORM\Column(type: 'date', nullable: true)]
+    #[Groups("network")]
+    private ?\DateTimeInterface $startDate = null;
+
+    #[ORM\Column(type: 'date', nullable: true)]
+    private ?\DateTimeInterface $endDate = null;
+
     public function getId(): ?int
     {
         return $this->id;
@@ -89,4 +111,23 @@ class NetworkOrganization
 
         return $this;
     }
+
+    public function getStartDate(): ?\DateTimeInterface {
+        return $this->startDate;
+    }
+
+    public function setStartDate(?\DateTime $startDate = null): self {
+        if($startDate == null) $startDate = new \DateTime();
+        $this->startDate = $startDate;
+        return $this;
+    }
+
+    public function getEndDate(): ?\DateTimeInterface {
+        return $this->endDate;
+    }
+
+    public function setEndDate(?\DateTime $endDate = null) :self {
+        $this->endDate = $endDate;
+        return $this;
+    }
 }

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

@@ -5,8 +5,10 @@ namespace App\Entity\Organization;
 
 use ApiPlatform\Core\Annotation\ApiResource;
 use ApiPlatform\Core\Annotation\ApiSubresource;
+use App\Entity\Billing\BillingSetting;
 use App\Entity\Core\BankAccount;
 use App\Entity\Core\ContactPoint;
+use App\Entity\Core\File;
 use App\Entity\Network\NetworkOrganization;
 use App\Repository\Organization\OrganizationRepository;
 use Doctrine\Common\Collections\ArrayCollection;
@@ -24,7 +26,7 @@ use Symfony\Component\Validator\Constraints as Assert;
             'security' => '(is_granted("ROLE_ORGANIZATION_VIEW") or is_granted("ROLE_ORGANIZATION")) and object.getId() == user.getOrganization().getId()'
         ],
         'put' => [
-            'security' => 'is_granted("ROLE_ORGANIZATION") and object.getId() == user.getOrganization()s.getId()'
+            'security' => 'is_granted("ROLE_ORGANIZATION") and object.getId() == user.getOrganization().getId()'
         ]
     ]
 )]
@@ -36,7 +38,7 @@ class Organization
     #[ORM\GeneratedValue]
     private ?int $id = null;
 
-    #[ORM\Column(length: 128)]
+    #[ORM\Column(length: 128, nullable: false)]
     public string $name;
 
     #[ORM\Column(length: 128)]
@@ -54,6 +56,7 @@ class Organization
     private Settings $settings;
 
     #[ORM\OneToMany(mappedBy: 'organization', targetEntity: NetworkOrganization::class, orphanRemoval: true)]
+    #[ApiSubresource]
     private Collection $networkOrganizations;
 
     #[ORM\OneToMany(mappedBy: 'parent', targetEntity: NetworkOrganization::class, orphanRemoval: true)]
@@ -63,6 +66,9 @@ class Organization
     #[ORM\JoinColumn(nullable: false)]
     private Parameters $parameters;
 
+    #[ORM\OneToOne(mappedBy: 'organization', cascade: ['persist', 'remove'], orphanRemoval: true)]
+    private BillingSetting $billingSetting;
+
     #[ORM\Column(length: 255, nullable: true)]
     private ?string $description = null;
 
@@ -96,6 +102,9 @@ class Organization
     #[ORM\Column(length: 255, nullable: true)]
     private ?string $twitter = null;
 
+    #[ORM\Column(length: 255, nullable: true)]
+    private ?string $youtube = null;
+
     #[ORM\Column(length: 255, nullable: true)]
     private ?string $instagram = null;
 
@@ -164,6 +173,18 @@ class Organization
     #[ORM\Column(nullable: true)]
     private ?int $cmsId = null;
 
+    #[ORM\ManyToOne(inversedBy: 'organizationLogos')]
+    #[ORM\JoinColumn(referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
+    private ?File $logo = null;
+
+    #[ORM\ManyToOne(inversedBy: 'organizationLogos')]
+    #[ORM\JoinColumn(referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
+    private ?File $image = null;
+
+    #[ORM\ManyToMany(targetEntity: TypeOfPractice::class, mappedBy: 'organizations')]
+    #[ApiSubresource]
+    private Collection $typeOfPractices;
+
     #[ORM\Column(nullable: true)]
     private ?string $otherPractice = null;
 
@@ -182,14 +203,20 @@ class Organization
     #[ORM\OneToMany(mappedBy: 'organization', targetEntity: OrganizationLicence::class, orphanRemoval: true)]
     private Collection $organizationLicences;
 
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: OrganizationArticle::class, orphanRemoval: true)]
+    #[ApiSubresource]
+    private Collection $organizationArticles;
+
     #[Pure] public function __construct()
     {
         $this->networkOrganizations = new ArrayCollection();
         $this->networkOrganizationChildren = new ArrayCollection();
+        $this->typeOfPractices = new ArrayCollection();
         $this->contactPoints = new ArrayCollection();
         $this->bankAccounts = new ArrayCollection();
         $this->organizationAddressPostals = new ArrayCollection();
         $this->organizationLicences = new ArrayCollection();
+        $this->organizationArticles = new ArrayCollection();
     }
 
     public function getId(): ?int
@@ -328,6 +355,18 @@ class Organization
         return $this;
     }
 
+    public function getBillingSetting(): BillingSetting
+    {
+        return $this->billingSetting;
+    }
+
+    public function setBillingSetting(BillingSetting $billingSetting): self
+    {
+        $this->billingSetting = $billingSetting;
+
+        return $this;
+    }
+
     public function getDescription(): ?string
     {
         return $this->description;
@@ -460,6 +499,18 @@ class Organization
         return $this;
     }
 
+    public function getYoutube(): ?string
+    {
+        return $this->youtube;
+    }
+
+    public function setYoutube(?string $youtube): self
+    {
+        $this->youtube = $youtube;
+
+        return $this;
+    }
+
     public function getInstagram(): ?string
     {
         return $this->instagram;
@@ -712,6 +763,52 @@ class Organization
         return $this;
     }
 
+    public function setLogo(?File $image):self
+    {
+        $this->logo = $image;
+        return $this;
+    }
+
+    public function getLogo(): ?File
+    {
+        return $this->logo;
+    }
+
+    public function setImage(?File $image):self
+    {
+        $this->image = $image;
+        return $this;
+    }
+
+    public function getImage(): ?File
+    {
+        return $this->image;
+    }
+
+    public function getTypeOfPractices(): Collection
+    {
+        return $this->typeOfPractices;
+    }
+
+    public function addTypeOfPractice(TypeOfPractice $typeOfPractice): self
+    {
+        if (!$this->typeOfPractices->contains($typeOfPractice)) {
+            $this->typeOfPractices[] = $typeOfPractice;
+            $typeOfPractice->addOrganization($this);
+        }
+
+        return $this;
+    }
+
+    public function removeTypeOfPractice(TypeOfPractice $typeOfPractice): self
+    {
+        if ($this->typeOfPractices->removeElement($typeOfPractice)) {
+            $typeOfPractice->removeOrganization($this);
+        }
+
+        return $this;
+    }
+
     public function getOtherPractice(): ?string
     {
         return $this->otherPractice;
@@ -825,4 +922,31 @@ class Organization
 
         return $this;
     }
+
+    public function getOrganizationArticles(): Collection
+    {
+        return $this->organizationArticles;
+    }
+
+    public function addOrganizationArticle(OrganizationArticle $organizationArticle): self
+    {
+        if (!$this->organizationArticles->contains($organizationArticle)) {
+            $this->organizationArticles[] = $organizationArticle;
+            $organizationArticle->setOrganization($this);
+        }
+
+        return $this;
+    }
+
+    public function removeOrganizationArticle(OrganizationArticle $organizationArticle): self
+    {
+        if ($this->organizationArticles->removeElement($organizationArticle)) {
+            // set the owning side to null (unless already changed)
+            if ($organizationArticle->getOrganization() === $this) {
+                $organizationArticle->setOrganization(null);
+            }
+        }
+
+        return $this;
+    }
 }

+ 30 - 5
src/Entity/Organization/OrganizationAddressPostal.php

@@ -4,30 +4,55 @@ declare(strict_types=1);
 namespace App\Entity\Organization;
 
 use ApiPlatform\Core\Annotation\ApiResource;
+use App\Annotation\OrganizationDefaultValue;
 use App\Entity\Core\AddressPostal;
 use App\Repository\Organization\OrganizationAddressPostalRepository;
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Component\Validator\Constraints as Assert;
-
-#[ApiResource]
+use Symfony\Component\Serializer\Annotation\Groups;
+use App\Validator\Organization as OpentalentAssert;
+
+#[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')"],
+    denormalizationContext: [
+        'groups' => ['address'],
+    ],
+    normalizationContext: [
+        'groups' => ['address'],
+    ],
+)]
 #[ORM\Entity(repositoryClass: OrganizationAddressPostalRepository::class)]
+#[OrganizationDefaultValue(fieldName: "organization")]
+#[OpentalentAssert\OrganizationAddressPostal]
 class OrganizationAddressPostal
 {
     #[ORM\Id]
     #[ORM\Column]
     #[ORM\GeneratedValue]
+    #[Groups("address")]
     private ?int $id = null;
 
     #[ORM\ManyToOne(inversedBy: 'organizationAddressPostals')]
     #[ORM\JoinColumn(nullable: false)]
-    private ?Organization $organization = null;
+    private Organization $organization;
 
     #[ORM\OneToOne(inversedBy: 'organizationAddressPostal', cascade: ['persist', 'remove'])]
     #[ORM\JoinColumn(nullable: false)]
-    private ?AddressPostal $addressPostal = null;
+    #[Groups("address")]
+    private AddressPostal $addressPostal;
 
     #[ORM\Column(length: 255)]
-    #[Assert\Choice(callback: ['\App\Enum\Core\AddressPostalTypeEnum', 'toArray'], message: 'invalid-address-postal-type')]
+    #[Assert\Choice(callback: ['\App\Enum\Organization\AddressPostalOrganizationTypeEnum', 'toArray'], message: 'invalid-address-postal-type')]
+    #[Groups("address")]
     private string $type;
 
     public function getId(): ?int

+ 95 - 0
src/Entity/Organization/OrganizationArticle.php

@@ -0,0 +1,95 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Entity\Organization;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use App\Repository\Organization\OrganizationArticleRepository;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * Fait le lien entre une Organization et un coup de projecteur
+ */
+#[ApiResource(
+    collectionOperations: [
+        "get" => ["security" => "is_granted('ROLE_ORGANIZATION_VIEW')"]
+    ],
+    itemOperations: [
+        'get' => [
+            'security' => 'is_granted("ROLE_ORGANIZATION_VIEW" and object.getOrganization().getId() == user.getOrganization().getId()'
+        ]
+    ],
+    attributes: ["security" => "is_granted('ROLE_ORGANIZATION')"]
+)]
+#[ORM\Entity(repositoryClass: OrganizationArticleRepository::class)]
+class OrganizationArticle
+{
+    #[ORM\Id]
+    #[ORM\Column]
+    #[ORM\GeneratedValue]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'organizationArticles')]
+    #[ORM\JoinColumn(nullable: true)]
+    private Organization $organization;
+
+    #[ORM\Column(length: 255)]
+    private string $title;
+
+    #[ORM\Column(length: 255)]
+    private string $link;
+
+    #[ORM\Column(type: 'date', nullable: true)]
+    private ?\DateTimeInterface $date = 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 getTitle(): ?string
+    {
+        return $this->title;
+    }
+
+    public function setTitle(?string $title): self
+    {
+        $this->title = $title;
+
+        return $this;
+    }
+
+    public function getLink(): ?string
+    {
+        return $this->link;
+    }
+
+    public function setLink(?string $link): self
+    {
+        $this->link = $link;
+
+        return $this;
+    }
+
+    public function getDate(): ?\DateTimeInterface {
+        return $this->date;
+    }
+
+    public function setDate(?\DateTime $date = null): self {
+        $this->date = $date;
+        return $this;
+    }
+}

+ 4 - 1
src/Entity/Organization/Parameters.php

@@ -8,7 +8,10 @@ use App\Repository\Organization\ParametersRepository;
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Component\Validator\Constraints as Assert;
 
-#[ApiResource]
+#[ApiResource(
+    collectionOperations: [],
+    itemOperations: ['get']
+)]
 #[ORM\Entity(repositoryClass: ParametersRepository::class)]
 class Parameters
 {

+ 113 - 0
src/Entity/Organization/TypeOfPractice.php

@@ -0,0 +1,113 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Entity\Organization;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use App\Repository\Organization\TypeOfPracticeRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ORM\Mapping as ORM;
+use JetBrains\PhpStorm\Pure;
+use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * Type des pratique d'une organisation
+ */
+#[ApiResource(
+    collectionOperations: [
+        'get' => [
+            'normalization_context' => [
+                'groups' => ['read']
+            ]
+        ]
+    ],
+    itemOperations: ['get'],
+    attributes:[
+        'pagination_enabled' => false
+    ]
+)]
+#[ORM\Entity(repositoryClass: TypeOfPracticeRepository::class)]
+class TypeOfPractice
+{
+    #[ORM\Id]
+    #[ORM\Column]
+    #[ORM\GeneratedValue]
+    #[Groups(["read"])]
+    private ?int $id = null;
+
+    #[ORM\Column(length: 255, nullable: true)]
+    #[Assert\Choice(callback: ['\App\Enum\Cotisation\TypeOfPracticeEnum', 'toArray'], message: 'invalid-name')]
+    #[Groups(["read"])]
+    private ?string $name = null;
+
+    #[ORM\Column(length: 255, nullable: true)]
+    #[Assert\Choice(callback: ['\App\Enum\Cotisation\CategoryTypeOfPracticeEnum', 'toArray'], message: 'invalid-category')]
+    #[Groups(["read"])]
+    private ?string $category = null;
+
+    #[ORM\ManyToMany(targetEntity: Organization::class, inversedBy: 'typeOfPractices')]
+    #[ORM\JoinTable(name: 'organization_type_of_practices')]
+    #[ORM\JoinColumn(name: 'typeofpractice_id', referencedColumnName: 'id')]
+    #[ORM\InverseJoinColumn(name: 'organization_id', referencedColumnName: 'id')]
+    private Collection $organizations;
+
+    #[Pure] public function __construct()
+    {
+        $this->organizations = new ArrayCollection();
+    }
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getName(): ?string
+    {
+        return $this->name;
+    }
+
+    public function setName(?string $name): self
+    {
+        $this->name = $name;
+
+        return $this;
+    }
+
+    public function getCategory(): ?string
+    {
+        return $this->category;
+    }
+
+    public function setCategory(?string $category): self
+    {
+        $this->category = $category;
+
+        return $this;
+    }
+
+    public function getOrganizations(): Collection
+    {
+        return $this->organizations;
+    }
+
+    public function addOrganization(Organization $organization): self
+    {
+        if (!$this->organizations->contains($organization)) {
+            $this->organizations[] = $organization;
+            $organization->addTypeOfPractice($this);
+        }
+
+        return $this;
+    }
+
+    public function removeOrganization(Organization $organization): self
+    {
+        if ($this->organizations->removeElement($organization)) {
+            $organization->removeTypeOfPractice($this);
+        }
+
+        return $this;
+    }
+}

+ 58 - 2
src/Entity/Person/Person.php

@@ -13,11 +13,17 @@ use Doctrine\ORM\Mapping as ORM;
 use JetBrains\PhpStorm\Pure;
 use Symfony\Component\Security\Core\User\UserInterface;
 use Symfony\Component\Validator\Constraints as Assert;
+use Symfony\Component\Serializer\Annotation\Groups;
 
 /**
  * Personne physique ou morale
  */
-#[ApiResource]
+#[ApiResource(
+    collectionOperations: [],
+    itemOperations: [
+        "get" => ["security" => "is_granted('ROLE_USERS_VIEW')"],
+    ]
+)]
 #[ORM\Entity(repositoryClass: PersonRepository::class)]
 class Person implements UserInterface
 {
@@ -35,14 +41,20 @@ class Person implements UserInterface
     private ?string $password = null;
 
     #[ORM\Column(length: 255, nullable: true)]
+    #[Groups(["access_people_ref", "access_address"])]
     private ?string $name = null;
 
     #[ORM\Column(length: 255, nullable: true)]
+    #[Groups(["access_people_ref", "access_address"])]
     private ?string $givenName = null;
 
     #[ORM\ManyToMany(targetEntity: ContactPoint::class, mappedBy: 'person')]
     private Collection $contactPoints;
 
+    #[ORM\OneToMany( mappedBy: 'person', targetEntity: PersonAddressPostal::class, orphanRemoval: true)]
+    #[Groups("access_address")]
+    private Collection $personAddressPostal;
+
     #[ORM\Column(nullable: true)]
     #[Assert\Choice(callback: ['\App\Enum\Person\GenderEnum', 'toArray'], message: 'invalid-gender')]
     private ?string $gender = null;
@@ -51,9 +63,13 @@ class Person implements UserInterface
     #[ORM\JoinColumn(referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
     private ?File $image = null;
 
+    #[ORM\Column(options: ['default' => true])]
+    private bool $isPhysical = true;
+
     #[Pure] public function __construct()
     {
         $this->contactPoints = new ArrayCollection();
+        $this->personAddressPostal = new ArrayCollection();
     }
 
     public function getId(): ?int
@@ -106,7 +122,7 @@ class Person implements UserInterface
         return $this;
     }
 
-    public function getSalt()
+    public function getSalt(): ?string
     {
         // not needed when using the "bcrypt" algorithm in security.yaml
     }
@@ -189,4 +205,44 @@ class Person implements UserInterface
     {
         return $this->image;
     }
+
+    public function getPersonAddressPostal(): Collection
+    {
+        return $this->personAddressPostal;
+    }
+
+    public function addPersonAddressPostal(PersonAddressPostal $personAddressPostal): self
+    {
+        if (!$this->personAddressPostal->contains($personAddressPostal)) {
+            $this->personAddressPostal[] = $personAddressPostal;
+            $personAddressPostal->setPerson($this);
+        }
+
+        return $this;
+    }
+
+    public function removePersonAddressPostal(PersonAddressPostal $personAddressPostal): self
+    {
+        if ($this->personAddressPostal->removeElement($personAddressPostal)) {
+            // set the owning side to null (unless already changed)
+            if ($personAddressPostal->getPerson() === $this) {
+                $personAddressPostal->setPerson(null);
+            }
+        }
+
+        return $this;
+    }
+
+    public function getIsPhysical(): ?bool
+    {
+        return $this->isPhysical;
+    }
+
+    public function setAdminAccess(bool $isPhysical): self
+    {
+        $this->isPhysical = $isPhysical;
+
+        return $this;
+    }
+
 }

+ 84 - 0
src/Entity/Person/PersonAddressPostal.php

@@ -0,0 +1,84 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Entity\Person;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use App\Entity\Core\AddressPostal;
+use App\Repository\Person\PersonAddressPostalRepository;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * Lien entre une Person et une AddressPostal
+ */
+#[ApiResource(
+    collectionOperations: [],
+    itemOperations: [
+        "get" => ["security" => "is_granted('ROLE_USERS_VIEW')"],
+    ]
+)]
+#[ORM\Entity(repositoryClass: PersonAddressPostalRepository::class)]
+class PersonAddressPostal
+{
+    #[ORM\Id]
+    #[ORM\Column]
+    #[ORM\GeneratedValue]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'personAddressPostal')]
+    #[ORM\JoinColumn(nullable: false)]
+    private Person $person;
+
+    #[ORM\OneToOne(inversedBy: 'personAddressPostal', cascade: ['persist', 'remove'])]
+    #[ORM\JoinColumn(nullable: false)]
+    #[Groups("access_address")]
+    private AddressPostal $addressPostal;
+
+    #[ORM\Column(length: 255)]
+    #[Assert\Choice(callback: ['\App\Enum\Person\AddressPostalPersonTypeEnum', 'toArray'], message: 'invalid-address-postal-type')]
+    #[Groups("access_address")]
+    private string $type;
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getPerson(): ?Person
+    {
+        return $this->person;
+    }
+
+    public function setPerson(Person $person): self
+    {
+        $this->person = $person;
+
+        return $this;
+    }
+
+    public function getAddressPostal(): ?AddressPostal
+    {
+        return $this->addressPostal;
+    }
+
+    public function setAddressPostal(AddressPostal $addressPostal): self
+    {
+        $this->addressPostal = $addressPostal;
+
+        return $this;
+    }
+
+    public function getType(): string
+    {
+        return $this->type;
+    }
+
+    public function setType(string $type): self
+    {
+        $this->type = $type;
+
+        return $this;
+    }
+}

+ 1 - 0
src/Entity/Traits/ActivityPeriodTrait.php

@@ -3,6 +3,7 @@ declare(strict_types=1);
 
 namespace App\Entity\Traits;
 
+use App\Annotation\DateTimeAware;
 use Doctrine\ORM\Mapping as ORM;
 
 trait ActivityPeriodTrait

+ 0 - 9
src/Enum/Core/ContactPointTypeEnum.php

@@ -15,13 +15,4 @@ class ContactPointTypeEnum extends Enum
     private const BILL = 'BILL';
     private const OTHER = 'OTHER';
     private const CONTACT = 'CONTACT';
-
-    public static function toArray(bool $type = false): array
-    {
-        if($type == 'person'){
-            return ['PRINCIPAL'=>self::PRINCIPAL,'OTHER'=>self::OTHER];
-        }else{
-            return parent::toArray();
-        }
-    }
 }

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

@@ -0,0 +1,18 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Enum\Cotisation;
+
+use MyCLabs\Enum\Enum;
+
+/**
+ * catgorie des types de pratiques
+ */
+class CategoryTypeOfPracticeEnum extends Enum
+{
+    private const CATEGORY_ORCHESTRE = 'CATEGORY_ORCHESTRE';
+    private const CATEGORY_AMBULATORY = 'CATEGORY_AMBULATORY';
+    private const CATEGORY_CHORUS = 'CATEGORY_CHORUS';
+    private const CATEGORY_BAND = 'CATEGORY_BAND';
+    private const CATEGORY_OTHER = 'CATEGORY_OTHER';
+}

+ 51 - 0
src/Enum/Cotisation/TypeOfPracticeEnum.php

@@ -0,0 +1,51 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Enum\Cotisation;
+
+use MyCLabs\Enum\Enum;
+
+/**
+ * noms des types de pratiques
+ */
+class TypeOfPracticeEnum extends Enum
+{
+    private const BATTERY_FANFARE = 'BATTERY_FANFARE';
+    private const BIG_BAND = 'BIG_BAND';
+    private const BRASS_BAND = 'BRASS_BAND';
+    private const MIXED_CHORUS = 'MIXED_CHORUS';
+    private const FEMAL_CHOIR = 'FEMAL_CHOIR';
+    private const MENS_CHOIR = 'MENS_CHOIR';
+    private const CHILDRENS_CHOIR = 'CHILDRENS_CHOIR';
+    private const ORCHESTRA_CLASS = 'ORCHESTRA_CLASS';
+    private const COPPER_BAND = 'COPPER_BAND';
+    private const JAZZ_BAND = 'JAZZ_BAND';
+    private const PERCUSSION_BAND = 'PERCUSSION_BAND';
+    private const PLUCKED_ORCHESTRA = 'PLUCKED_ORCHESTRA';
+    private const FOLKLORIC_BAND = 'FOLKLORIC_BAND';
+    private const VOCAL_BAND_UP_16 = 'VOCAL_BAND_UP_16';
+    private const FIFE_AND_DRUM = 'FIFE_AND_DRUM';
+    private const CURRENT_MUSIC_GROUP = 'CURRENT_MUSIC_GROUP';
+    private const CHAMBER_MUSIC_ENSEMBLE = 'CHAMBER_MUSIC_ENSEMBLE';
+    private const TRADITIONAL_MUSIC_ENSEMBLE = 'TRADITIONAL_MUSIC_ENSEMBLE';
+    private const VARIOUS_ORCHESTRA = 'VARIOUS_ORCHESTRA';
+    private const ACCORDION_ORCHESTRA = 'ACCORDION_ORCHESTRA';
+    private const HARMONY_ORCHESTRA = 'HARMONY_ORCHESTRA';
+    private const FANFARE_BAND = 'FANFARE_BAND';
+    private const SYMPHONY_ORCHESTRA = 'SYMPHONY_ORCHESTRA';
+    private const VIOLIN_BAND = 'VIOLIN_BAND';
+    private const SAXOPHONES_BAND = 'SAXOPHONES_BAND';
+    private const HUNTING_HORNS = 'HUNTING_HORNS';
+    private const STRING_ORCHESTRA = 'STRING_ORCHESTRA';
+    private const FLUTE_ENSEMBLE = 'FLUTE_ENSEMBLE';
+    private const CLARINET_CHOIR = 'CLARINET_CHOIR';
+    private const PHILHARMONIC_ORCHESTRA = 'PHILHARMONIC_ORCHESTRA';
+    private const BANDAS = 'BANDAS';
+    private const BAGAD = 'BAGAD';
+    private const BATTUCADA = 'BATTUCADA';
+    private const MARCHING_BAND = 'MARCHING_BAND';
+    private const EDUCATION = "EDUCATION";
+    private const CHEERLEADER = "CHEERLEADER";
+    private const TROOP = "TROOP";
+    private const OTHER = "OTHER";
+}

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

@@ -6,7 +6,7 @@ namespace App\Enum\Organization;
 use MyCLabs\Enum\Enum;
 
 /**
- * Type d'adresse postale
+ * Type d'adresse postale pour une organization
  */
 class AddressPostalOrganizationTypeEnum extends Enum
 {

+ 15 - 0
src/Enum/Person/AddressPostalPersonTypeEnum.php

@@ -0,0 +1,15 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Enum\Person;
+
+use MyCLabs\Enum\Enum;
+
+/**
+ * Type d'adresse postale pour une Person
+ */
+class AddressPostalPersonTypeEnum extends Enum
+{
+    private const ADDRESS_PRINCIPAL = 'ADDRESS_PRINCIPAL';
+    private const ADDRESS_OTHER = 'ADDRESS_OTHER';
+}

+ 38 - 0
src/EventListener/DoctrineFilter/DoctrineFilterListener.php

@@ -0,0 +1,38 @@
+<?php
+declare(strict_types=1);
+
+namespace App\EventListener\DoctrineFilter;
+
+use App\Service\Utils\DateTimeConstraint;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpKernel\Event\RequestEvent;
+use Symfony\Component\Security\Core\Security;
+
+/**
+ * Classe DoctrineFilterListener qui permet d'assurer l'injection de dépendance pour le SQL Filter
+ */
+class DoctrineFilterListener
+{
+    public function __construct(
+        private EntityManagerInterface $entityManager,
+        private DateTimeConstraint $dateTimeConstraint,
+        private Security $security,
+        private RequestStack $requestStack
+    )
+    {
+    }
+
+    public function onKernelRequest(RequestEvent $event)
+    {
+        if (!$event->isMainRequest()) {
+            // 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));
+
+        $filter->setDateTimeConstraint($this->dateTimeConstraint);
+    }
+}

+ 17 - 0
src/EventListener/Helper.php

@@ -0,0 +1,17 @@
+<?php
+declare(strict_types=1);
+
+namespace App\EventListener;
+
+use Doctrine\ORM\EntityManagerInterface;
+
+
+/**
+ * Trait Helper qui met à disposition des fonctions d'aide pour les EventListeners
+ */
+trait Helper
+{
+    private function hasChangeField(EntityManagerInterface $entityManager, $entity, string $field){
+        return key_exists($field, $entityManager->getUnitOfWork()->getEntityChangeSet($entity));
+    }
+}

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

@@ -0,0 +1,68 @@
+<?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());
+        }
+    }
+}

+ 103 - 0
src/Filter/DoctrineFilter/DateTimeFilter.php

@@ -0,0 +1,103 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Filter\DoctrineFilter;
+
+use App\Annotation\DateTimeConstraintAware;
+use App\Service\Utils\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
+{
+    private DateTimeConstraint $dateTimeConstraint;
+
+    /**
+     * 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 '';
+
+        $dateTimeConstraintAware = $targetEntity->getReflectionClass()->getAttributes(DateTimeConstraintAware::class)[0] ?? null;
+        $startFieldName = $dateTimeConstraintAware?->getArguments()['startDateFieldName'] ?? null;
+        $endFieldName = $dateTimeConstraintAware?->getArguments()['endDateFieldName'] ?? null;
+        if ($startFieldName === '' || is_null($startFieldName) || $endFieldName === '' || is_null($endFieldName)) {
+            return '';
+        }
+
+        $accessId = intval(StringsUtils::unquote($this->getParameter('accessId')));
+        $constraints = $this->dateTimeConstraint->invoke($accessId);
+
+        $fields = [
+            DateTimeConstraint::START_KEY => $startFieldName,
+            DateTimeConstraint::END_KEY => $endFieldName
+        ];
+      
+        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
+     */
+    public function setDateTimeConstraint(DateTimeConstraint $dateTimeConstraint){
+        $this->dateTimeConstraint = $dateTimeConstraint;
+    }
+}

+ 41 - 0
src/Filter/Person/FullNameFilter.php

@@ -0,0 +1,41 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Filter\Person;
+
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractFilter;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
+use Doctrine\ORM\QueryBuilder;
+
+class FullNameFilter extends AbstractFilter{
+    protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
+    {
+        if ($property !== 'fullname') {
+            return;
+        }
+        $alias = $queryBuilder->getRootAliases()[0];
+        $queryBuilder
+            ->innerJoin(sprintf('%s.person', $alias), 'person')
+            ->andWhere(sprintf('person.name LIKE :search OR person.givenName LIKE :search', $alias, $alias))
+            ->setParameter('search', '%'.$value.'%');
+    }
+
+    /**
+     * API docs
+     * @param string $resourceClass
+     * @return array[]
+     */
+    public function getDescription(string $resourceClass): array
+    {
+        return [
+            'fullname' => [
+                'property' => null,
+                'type' => 'string',
+                'required' => false,
+                'openapi' => [
+                    'description' => 'Rechercher parmi les champs name et givenName',
+                ],
+            ]
+        ];
+    }
+}

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

@@ -95,6 +95,8 @@ class AccessRepository extends ServiceEntityRepository implements UserLoaderInte
 
     public function hasGotFunctionAtThisDate(Access $access, $function, \DateTime $date): bool
     {
+        $this->_em->getFilters()->disable('date_time_filter');
+
         $qb = $this->createQueryBuilder('access');
         $qb
             ->innerJoin('access.organizationFunction', 'organization_function')
@@ -106,6 +108,10 @@ class AccessRepository extends ServiceEntityRepository implements UserLoaderInte
         ;
         DateConditions::addDateInPeriodCondition($qb, 'organization_function', $date->format('Y-m-d'));
 
-        return count($qb->getQuery()->getResult()) > 0;
+        $result = count($qb->getQuery()->getResult()) > 0;
+
+        $this->_em->getFilters()->enable('date_time_filter');
+
+        return $result;
     }
 }

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

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

+ 38 - 0
src/Repository/Core/ContactPointRepository.php

@@ -4,6 +4,8 @@ declare(strict_types=1);
 namespace App\Repository\Core;
 
 use App\Entity\Core\ContactPoint;
+use App\Entity\Organization\Organization;
+use App\Entity\Person\Person;
 use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
 use Doctrine\Persistence\ManagerRegistry;
 
@@ -20,4 +22,40 @@ class ContactPointRepository extends ServiceEntityRepository
         parent::__construct($registry, ContactPoint::class);
     }
 
+    /**
+     * 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
+     */
+    public function getByTypeAndOrganization(String $type, Organization $organization): array | null{
+        return $this->createQueryBuilder('contact_point')
+            ->innerJoin('contact_point.organization', 'organization')
+            ->where('contact_point.contactType = :type')
+            ->andWhere('organization.id = :organizationId')
+            ->setParameter('type', $type)
+            ->setParameter('organizationId', $organization->getId())
+            ->getQuery()
+            ->getResult()
+            ;
+    }
+
+    /**
+     * 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
+     */
+    public function getByTypeAndPerson(String $type, Person $person): array | null{
+        return $this->createQueryBuilder('contact_point')
+            ->innerJoin('contact_point.person', 'person')
+            ->where('contact_point.contactType = :type')
+            ->andWhere('person.id = :personId')
+            ->setParameter('type', $type)
+            ->setParameter('personId', $person->getId())
+            ->getQuery()
+            ->getResult()
+            ;
+    }
+
 }

+ 4 - 4
src/Repository/Cotisation/CotisationApiResourcesRepository.php

@@ -7,7 +7,7 @@ namespace App\Repository\Cotisation;
 use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\Query\ResultSetMapping;
 
-final class CotisationApiResourcesRepository
+class CotisationApiResourcesRepository
 {
     public function __construct(private EntityManagerInterface $adminassosEntityManager)
     {
@@ -17,9 +17,9 @@ final class CotisationApiResourcesRepository
      * Récupère l'état de la cotisation pour une structure et une année
      * @param int $organizationId
      * @param int $year
-     * @return string|null
+     * @return int|null
      */
-    public function getAffiliationState(int $organizationId, int $year): string|null {
+    public function getAffiliationState(int $organizationId, int $year): int|null {
         $rsm = new ResultSetMapping();
         $rsm->addScalarResult('oa_miscellaneous_state_sta', 'oa_miscellaneous_state_sta');
 
@@ -30,7 +30,7 @@ final class CotisationApiResourcesRepository
         $result = $query->getOneOrNullResult();
 
         if (!empty($result)) {
-            return $result['oa_miscellaneous_state_sta'];
+            return intval($result['oa_miscellaneous_state_sta']);
         }
         return null;
     }

+ 18 - 0
src/Repository/Organization/OrganizationAddressPostalRepository.php

@@ -3,6 +3,7 @@ declare(strict_types=1);
 
 namespace App\Repository\Organization;
 
+use App\Entity\Organization\Organization;
 use App\Entity\Organization\OrganizationAddressPostal;
 use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
 use Doctrine\Persistence\ManagerRegistry;
@@ -19,4 +20,21 @@ class OrganizationAddressPostalRepository extends ServiceEntityRepository
     {
         parent::__construct($registry, OrganizationAddressPostal::class);
     }
+
+    /**
+     * Récupération des adresses postal d'une organization et d'un type précis
+     * @param String $type
+     * @param Organization $organization
+     * @return array|null
+     */
+    public function getByType(String $type, Organization $organization): array | null{
+        return $this->createQueryBuilder('organizationAddressPostal')
+            ->where('organizationAddressPostal.type = :type')
+            ->andWhere('organizationAddressPostal.organization = :organization')
+            ->setParameter('type', $type)
+            ->setParameter('organization', $organization)
+            ->getQuery()
+            ->getResult()
+            ;
+    }
 }

+ 18 - 0
src/Repository/Organization/OrganizationArticleRepository.php

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

+ 18 - 0
src/Repository/Organization/TypeOfPracticeRepository.php

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

+ 16 - 0
src/Repository/Person/PersonAddressPostalRepository.php

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

+ 2 - 1
src/Security/Voter/BankAccountVoter.php

@@ -17,7 +17,7 @@ class BankAccountVoter extends Voter
 
     protected function supports($attribute, $subject): bool
     {
-        return in_array($attribute, ['BANK_ACCOUNT_READ', 'BANK_ACCOUNT_EDIT'])
+        return in_array($attribute, ['BANK_ACCOUNT_READ', 'BANK_ACCOUNT_EDIT', 'BANK_ACCOUNT_DELETE'])
             && $subject instanceof BankAccount;
     }
 
@@ -44,6 +44,7 @@ class BankAccountVoter extends Voter
                 }
                 break;
             case 'BANK_ACCOUNT_EDIT':
+            case 'BANK_ACCOUNT_DELETE':
                 if($subject->getOrganization()->count() === 1){
                     return $this->security->isGranted('ROLE_ORGANIZATION')
                         && $subject->getOrganization()->current()->getId() === $user->getOrganization()->getId();

+ 2 - 2
src/Security/Voter/ContactPointVoter.php

@@ -17,7 +17,7 @@ class ContactPointVoter extends Voter
 
     protected function supports($attribute, $subject): bool
     {
-        return in_array($attribute, ['CONTACT_POINT_READ', 'CONTACT_POINT_EDIT'])
+        return in_array($attribute, ['CONTACT_POINT_READ', 'CONTACT_POINT_EDIT', 'CONTACT_POINT_DELETE'])
             && $subject instanceof ContactPoint;
     }
 
@@ -35,7 +35,6 @@ class ContactPointVoter extends Voter
         if (!$user instanceof UserInterface) {
             return false;
         }
-
         switch ($attribute) {
             case 'CONTACT_POINT_READ':
                 if($subject->getOrganization()->count() === 1){
@@ -44,6 +43,7 @@ class ContactPointVoter extends Voter
                 }
                 break;
             case 'CONTACT_POINT_EDIT':
+            case 'CONTACT_POINT_DELETE':
                 if($subject->getOrganization()->count() === 1){
                     return $this->security->isGranted('ROLE_ORGANIZATION')
                         && $subject->getOrganization()->current()->getId() === $user->getOrganization()->getId();

+ 63 - 0
src/Serializer/DefaultNormalizer.php

@@ -0,0 +1,63 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Serializer;
+
+use App\Entity\Access\Access;
+use App\Service\Utils\EntityUtils;
+use Symfony\Component\Security\Core\Security;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+use Symfony\Component\Serializer\SerializerAwareInterface;
+use Symfony\Component\Serializer\SerializerInterface;
+
+/**
+ * Serializer par défaut
+ */
+final class DefaultNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
+{
+    public function __construct(
+        private NormalizerInterface $decorated,
+        private EntityUtils $entityUtils,
+        private Security $security
+    )
+    {
+        if (!$this->decorated instanceof DenormalizerInterface) {
+            throw new \InvalidArgumentException(sprintf('The decorated normalizer must implement the %s.', DenormalizerInterface::class));
+        }
+    }
+
+    public function supportsNormalization($data, $format = null)
+    {
+        return $this->decorated->supportsNormalization($data, $format);
+    }
+
+    public function normalize($object, $format = null, array $context = [])
+    {
+        $data = $this->decorated->normalize($object, $format, $context);
+        return $data;
+    }
+
+    public function supportsDenormalization($data, $type, $format = null)
+    {
+        return $this->decorated->supportsDenormalization($data, $type, $format);
+    }
+
+    public function denormalize($data, $class, $format = null, array $context = [])
+    {
+        $entity = $this->decorated->denormalize($data, $class, $format, $context);
+
+        /** @var Access $access */
+        $access = $this->security->getUser();
+        $this->entityUtils->defaultValueSettersByAccess($entity, $access);
+
+        return $entity;
+    }
+
+    public function setSerializer(SerializerInterface $serializer)
+    {
+        if($this->decorated instanceof SerializerAwareInterface) {
+            $this->decorated->setSerializer($serializer);
+        }
+    }
+}

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

@@ -14,6 +14,9 @@ use App\Test\Service\Organization\UtilsTest;
  */
 class Utils
 {
+    const START_DATE_KEY = 'dateStart';
+    const END_DATE_KEY = 'dateEnd';
+
     /**
      * Test si l'organisation est considérée comme une structure == n'a pas un produit manager
      * @param Organization $organization
@@ -86,4 +89,30 @@ class Utils
 
         else return $year;
     }
+
+    /**
+     * Fonction permettant de récupérer les dates de début et de fin d'activité d'une structure selon une année
+     * @param Organization $organization
+     * @param int $year
+     * @return \DateTime[]
+     * @throws \Exception
+     */
+    public static function getActivityPeriodsSwitchYear(Organization $organization, int $year): array
+    {
+        $musicalDate = $organization->getParameters()->getMusicalDate();
+
+        if (empty($musicalDate)) {
+            $dateStart = new \DateTime($year . "-09-01");
+            $dateEnd = new \DateTime(($year + 1) . "-08-31");
+        } else {
+            $dateStart = new \DateTime($year . "-" . $musicalDate->format('m') . "-" . $musicalDate->format('d'));
+            $dateEnd = clone($dateStart);
+            $dateEnd->add(new \DateInterval('P1Y'))->sub(new \DateInterval('P1D'));
+        }
+
+        return [
+            self::START_DATE_KEY => $dateStart->format('Y-m-d'),
+            self::END_DATE_KEY => $dateEnd->format('Y-m-d')
+        ];
+    }
 }

+ 236 - 0
src/Service/Utils/DateTimeConstraint.php

@@ -0,0 +1,236 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Utils;
+
+use App\Entity\Access\Access;
+use App\Service\Organization\Utils as organizationUtils;
+use App\Tests\Service\Utils\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
+{
+    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,
+    )
+    { }
+
+    /**
+     * 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->getCustomPeriods($historical['dateStart'], $historical['dateEnd']);
+            //Une période "Custom" reviens à savoir quels sont les éléments actif durant le "présent" de la période custom, donc
+            //on peut utiliser le presentConstraint avec les periods custom
+            $contraints = $this->addConstraint($contraints, $this->presentConstraint($periods));
+        }else{
+            $periods = $this->getPeriods($access);
+            if($historical['present']) $contraints = $this->addConstraint($contraints, $this->presentConstraint($periods));
+            if($historical['past']) $contraints = $this->addConstraint($contraints, $this->pastConstraint($periods));
+            if($historical['future']) $contraints = $this->addConstraint($contraints, $this->futurConstraint($periods));
+        }
+        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
+     * @param string $dateEnd
+     * @return string[]
+     */
+    private function getCustomPeriods(string $dateStart, string $dateEnd): array{
+        return [
+            organizationUtils::START_DATE_KEY => $dateStart,
+            organizationUtils::END_DATE_KEY => $dateEnd
+        ];
+    }
+
+    /**
+     * Fonction permettant de récupérer les périodes de début et de fin d'affichage
+     * @param Access $access
+     * @return array
+     * @throws \Exception
+     * @see DateTimeConstraintTest::testGetPeriodsToday()
+     */
+    private function getPeriods(Access $access): array{
+        $organization = $access->getOrganization();
+        $activityYear = $access->getActivityYear();
+        $currentActivityYear = organizationUtils::getOrganizationCurrentActivityYear($organization);
+
+        $periods = organizationUtils::getActivityPeriodsSwitchYear($organization, $activityYear);
+        //Si l'année courante est l'année d'affichage choisie par l'utilisateur, alors la date de début est aujourd'hui
+        if($activityYear === $currentActivityYear){
+            $today = new \DateTime('now');
+            $periods[organizationUtils::START_DATE_KEY] = $today->format('Y-m-d');
+        }
+
+        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
+     * ET
+     *  - la date de fin est plus grande ou égale (>=) à la date de fin de période à afficher OU que la date de fin n'est pas remplie (NULL)
+     * @param $periods
+     * @return array
+     * @see DateTimeConstraintTest::testPresentConstrain()
+     */
+    private function presentConstraint(array $periods): array{
+        return [
+          self::START_KEY => [
+              $periods[organizationUtils::END_DATE_KEY] => self::INF + self::EQUAL
+          ],
+          self::END_KEY => [
+              $periods[organizationUtils::START_DATE_KEY] => self::SUP + self::EQUAL,
+              self::NULL_VALUE => self::NULL
+          ]
+        ];
+    }
+
+    /**
+     * Une période est dans le passée si :
+     * - la date de fin est plus petite (<) que la date de début de période à afficher
+     * @param $periods
+     * @return array
+     * @see DateTimeConstraintTest::testPastConstrain()
+     */
+    private function pastConstraint($periods): array{
+        return [
+            self::END_KEY => [
+                $periods[organizationUtils::START_DATE_KEY] => self::INF
+            ]
+        ];
+    }
+
+    /**
+     * Une période est dans le future si :
+     * - la date de début est plus grande (>) que la date de fin de période à afficher
+     * @param $periods
+     * @return array
+     * @see DateTimeConstraintTest::testFuturConstrain()
+     */
+    private function futurConstraint($periods): array{
+        return [
+            self::START_KEY => [
+                $periods[organizationUtils::END_DATE_KEY] => self::SUP
+            ]
+        ];
+    }
+}

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

@@ -0,0 +1,33 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Utils;
+
+use App\Annotation\OrganizationDefaultValue;
+use App\Entity\Access\Access;
+
+/**
+ * Class EntityUtils : Gestion des valeurs par défauts devant être présentes dans les entités.
+ * @package App\Service\Utils
+ */
+class EntityUtils
+{
+    public function defaultValueSettersByAccess($entity, Access $access)
+    {
+        $this->organizationDefaultValue($entity, $access);
+    }
+
+    /**
+     * @param $entity
+     * @throws \ReflectionException
+     */
+    private function organizationDefaultValue($entity, Access $access)
+    {
+        $reflection = new \ReflectionClass($entity::class);
+        $organizationFaultValue = $reflection->getAttributes(OrganizationDefaultValue::class)[0] ?? null;
+        $fieldName = $organizationFaultValue?->getArguments()['fieldName'] ?? null;
+        if($fieldName){
+            $entity->{sprintf('set%s', ucfirst($fieldName))}(...[$access->getOrganization()]);
+        }
+    }
+}

+ 3 - 3
src/Service/Utils/Reflection.php

@@ -34,15 +34,15 @@ class Reflection
     /**
      * Appel une fonction avec ses paramètres  depuis le nom d'une classe
      * @param string $serviceName
-     * @param string $method
+     * @param string $methodName
      * @param array $parameters
      * @return mixed
      * @throws \ReflectionException
      */
-    public function dynamicInvokeClassWithArgsAndMethod(string $className, string $method, array $parameters = []): mixed
+    public function dynamicInvokeClassWithArgsAndMethod(string $className, string $methodName, array $parameters = []): mixed
     {
         $reflection = new \ReflectionClass($className);
-        $method = $reflection->getMethod($method);
+        $method = $reflection->getMethod($methodName);
         if($method->isStatic()){
             return $method->invoke(null, $parameters);
         }else{

+ 23 - 0
src/Service/Utils/StringsUtils.php

@@ -0,0 +1,23 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Utils;
+
+use App\Tests\Service\Utils\StringsUtilsTest;
+
+/**
+ * Class StringsUtils : méthodes d'aide pour la gestion de string.
+ * @package App\Service\Utils
+ */
+class StringsUtils
+{
+    /**
+     * Supprime les quotes d'une chaine de caractères
+     * @param string $str
+     * @return string
+     * @see StringsUtilsTest::testUnquote()
+     */
+    public static function unquote(string $str): string {
+        return str_replace("'", "", $str);
+    }
+}

+ 17 - 0
src/Validator/Core/ContactPoint.php

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

+ 42 - 0
src/Validator/Core/ContactPointValidator.php

@@ -0,0 +1,42 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Validator\Core;
+
+use App\Enum\Core\ContactPointTypeEnum;
+use App\Repository\Core\ContactPointRepository;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use App\Entity\Core\ContactPoint;
+
+/**
+ * Classe control qu'une seul et même type de point de contact est autorisé pour chaque owner (organization, person, place)
+ */
+class ContactPointValidator extends ConstraintValidator
+{
+    public function __construct(private ContactPointRepository $contactPointRepository){}
+
+    public function validate($value, Constraint $constraint)
+    {
+        /** @var ContactPoint $contactPoint */
+        $contactPoint = $value;
+
+        // si le type est autre, on valide
+        if($contactPoint->getContactType() === ContactPointTypeEnum::OTHER()->getValue())
+            return;
+
+        $contactPointByType = [];
+        if($contactPoint->getOrganization())
+            $contactPointByType = $this->contactPointRepository->getByTypeAndOrganization($contactPoint->getContactType(), $contactPoint->getOrganization()->first());
+        else if($contactPoint->getPerson())
+            $contactPointByType = $this->contactPointRepository->getByTypeAndPerson($contactPoint->getContactType(), $contactPoint->getPerson()->first());
+
+        //Si le nombre de point de contact du type est supérieur à 1, OU si le nombre est égale a 1 ET que l'id du point de contact n'est pas celui en cours : invalide.
+        if(count($contactPointByType) > 1 || (count($contactPointByType) === 1 && $contactPointByType[0]->getId() !== $contactPoint->getId())){
+            $this->context->buildViolation($constraint->message)
+                ->setParameter('{{ type }}', $contactPoint->getContactType())
+                ->atPath('contactType')
+                ->addViolation();
+        }
+    }
+}

+ 17 - 0
src/Validator/Organization/OrganizationAddressPostal.php

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

+ 37 - 0
src/Validator/Organization/OrganizationAddressPostalValidator.php

@@ -0,0 +1,37 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Validator\Organization;
+
+use App\Enum\Organization\AddressPostalOrganizationTypeEnum;
+use App\Repository\Organization\OrganizationAddressPostalRepository;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use App\Entity\Organization\OrganizationAddressPostal;
+
+/**
+ * Classe control qu'une seul et même type d'adresse est autorisé pour les adresses d'organization (à part les "adresses autres")
+ */
+class OrganizationAddressPostalValidator extends ConstraintValidator
+{
+    public function __construct(private OrganizationAddressPostalRepository $organizationAddressPostalRepository){}
+
+    public function validate($value, Constraint $constraint)
+    {
+        /** @var OrganizationAddressPostal $organizationAddressPostal */
+        $organizationAddressPostal = $value;
+
+        // si le type est adresse autre, on valide
+        if($organizationAddressPostal->getType() === AddressPostalOrganizationTypeEnum::ADDRESS_OTHER()->getValue())
+            return;
+
+        $addressesByType = $this->organizationAddressPostalRepository->getByType($organizationAddressPostal->getType(), $organizationAddressPostal->getOrganization());
+        //Si le nombre d'adress du type est supérieur à 1, OU si le nombre est égale a 1 ET que l'id de l'adresse n'est pas celui en cours : invalide.
+        if(count($addressesByType) > 1 || (count($addressesByType) === 1 && $addressesByType[0]->getId() !== $organizationAddressPostal->getId())){
+            $this->context->buildViolation($constraint->message)
+                ->setParameter('{{ type }}', $organizationAddressPostal->getType())
+                ->atPath('type')
+                ->addViolation();
+        }
+    }
+}

+ 36 - 3
symfony.lock

@@ -25,6 +25,9 @@
     "composer/package-versions-deprecated": {
         "version": "1.11.99"
     },
+    "cyclonedx/cyclonedx-library": {
+        "version": "v1.0.3"
+    },
     "cyclonedx/cyclonedx-php-composer": {
         "version": "v3.4.1"
     },
@@ -146,6 +149,9 @@
             "config/packages/lexik_jwt_authentication.yaml"
         ]
     },
+    "monolog/monolog": {
+        "version": "2.3.5"
+    },
     "myclabs/php-enum": {
         "version": "1.7.7"
     },
@@ -248,6 +254,18 @@
             "bin/console"
         ]
     },
+    "symfony/debug-bundle": {
+        "version": "5.3",
+        "recipe": {
+            "repo": "github.com/symfony/recipes",
+            "branch": "master",
+            "version": "4.1",
+            "ref": "0ce7a032d344fb7b661cd25d31914cd703ad445b"
+        },
+        "files": [
+            "config/packages/dev/debug.yaml"
+        ]
+    },
     "symfony/dependency-injection": {
         "version": "v5.1.7"
     },
@@ -334,6 +352,24 @@
             "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
         }
     },
+    "symfony/monolog-bridge": {
+        "version": "v5.3.7"
+    },
+    "symfony/monolog-bundle": {
+        "version": "3.7",
+        "recipe": {
+            "repo": "github.com/symfony/recipes",
+            "branch": "master",
+            "version": "3.7",
+            "ref": "a7bace7dbc5a7ed5608dbe2165e0774c87175fe6"
+        },
+        "files": [
+            "config/packages/dev/monolog.yaml",
+            "config/packages/prod/deprecations.yaml",
+            "config/packages/prod/monolog.yaml",
+            "config/packages/test/monolog.yaml"
+        ]
+    },
     "symfony/orm-pack": {
         "version": "v2.0.0"
     },
@@ -379,9 +415,6 @@
     "symfony/polyfill-php81": {
         "version": "v1.23.0"
     },
-    "symfony/profiler-pack": {
-        "version": "v1.0.5"
-    },
     "symfony/property-access": {
         "version": "v5.1.7"
     },

+ 76 - 0
tests/Filter/DoctrineFilter/DateTimeFilterTest.php

@@ -0,0 +1,76 @@
+<?php
+namespace App\Tests\Filter\DoctrineFilter;
+
+use App\Filter\DoctrineFilter\DateTimeFilter;
+use App\Tests\TestToolsTrait;
+use Doctrine\ORM\EntityManagerInterface;
+use PHPUnit\Framework\TestCase;
+
+
+class DateTimeFilterTest extends TestCase
+{
+    use TestToolsTrait;
+
+    private DateTimeFilter $dateTimeFilter;
+
+    public function setUp(): void
+    {
+        $em = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
+        $this->dateTimeFilter = new DateTimeFilter($em);
+    }
+
+    /**
+     * @see DateTimeFilter::constructQuery()
+     */
+    public function testConstructQuery():void
+    {
+        $queryExpected = "(o.startDate <= '2021-12-20') AND (o.endDate > '2021-12-20' OR o.endDate IS NULL)";
+        $this->assertEquals($queryExpected, $this->invokeMethod($this->dateTimeFilter, 'constructQuery', [
+            [
+                'start' => [
+                    '2021-12-20' => [4]
+                ],
+                'end' => [
+                    '2021-12-20' => [5],
+                    'NULL' => [0]
+                ]
+            ],
+            'o',
+            [
+                'start' => 'startDate',
+                'end' => 'endDate'
+            ]
+        ]));
+
+        $queryExpected = "(o.startDate < '2021-12-20' OR o.startDate > '2021-12-30') AND (o.endDate < '2022-01-20')";
+        $this->assertEquals($queryExpected, $this->invokeMethod($this->dateTimeFilter, 'constructQuery', [
+            [
+                'start' => [
+                    '2021-12-20' => [1],
+                    '2021-12-30' => [5]
+                ],
+                'end' => [
+                    '2022-01-20' => [1]
+                ]
+            ],
+            'o',
+            [
+                'start' => 'startDate',
+                'end' => 'endDate'
+            ]
+        ]));
+    }
+
+    /**
+     * @see DateTimeFilter::getArithmeticValue()
+     */
+    public function testGetArithmeticValue():void
+    {
+        $this->assertEquals('<', $this->invokeMethod($this->dateTimeFilter, 'getArithmeticValue', [1]));
+        $this->assertEquals('<=', $this->invokeMethod($this->dateTimeFilter, 'getArithmeticValue', [4]));
+        $this->assertEquals('=', $this->invokeMethod($this->dateTimeFilter, 'getArithmeticValue', [3]));
+        $this->assertEquals('>=', $this->invokeMethod($this->dateTimeFilter, 'getArithmeticValue', [8]));
+        $this->assertEquals('>', $this->invokeMethod($this->dateTimeFilter, 'getArithmeticValue', [5]));
+        $this->assertEquals(null, $this->invokeMethod($this->dateTimeFilter, 'getArithmeticValue', [0]));
+    }
+}

+ 8 - 3
tests/Service/Cotisation/UtilsTest.php

@@ -22,10 +22,14 @@ class UtilsTest extends TestCase
     private OrganizationUtils $organizationUtilsMock;
 
     public function getOrganizationMock(): Organization{
-        $organizationMock = $this->getMockBuilder(Organization::class)->getMock();
-        return $organizationMock
+        $organizationMock = $this->getMockBuilder(Organization::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $organizationMock
             ->method('getId')
             ->willReturn(1);
+        return $organizationMock;
+
     }
 
     public function getUtilsInstance(){
@@ -59,6 +63,7 @@ class UtilsTest extends TestCase
         $this->cotisationApiResourcesRepositoryMock =
             $this
                 ->getMockBuilder(CotisationApiResourcesRepository::class)
+                ->disableOriginalConstructor()
                 ->getMock();
     }
 
@@ -327,7 +332,7 @@ class UtilsTest extends TestCase
 
         $this->cotisationApiResourcesRepositoryMock
             ->method('isNotDGVCustomer')
-            ->with($organizationMock->getId(), $year)
+            ->with($organizationMock->getId())
             ->willReturn(true);
 
         $this->assertEquals(AlertStateEnum::ADVERTISINGINSURANCE()->getValue(), $utils->getAlertState($organizationMock, $year) );

+ 220 - 0
tests/Service/Utils/DateTimeConstraintTest.php

@@ -0,0 +1,220 @@
+<?php
+namespace App\Tests\Service\Utils;
+
+use App\Entity\Access\Access;
+use App\Entity\Organization\Organization;
+use App\Entity\Organization\Parameters;
+use App\Service\Utils\DateTimeConstraint;
+use App\Tests\TestToolsTrait;
+use Doctrine\ORM\EntityManagerInterface;
+use PHPUnit\Framework\TestCase;
+
+class DateTimeConstraintTest extends TestCase
+{
+   use TestToolsTrait;
+
+   private DateTimeConstraint $dateTimeConstraint;
+   private array $periods;
+
+   public function setUp(): void
+   {
+       $em = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
+       $this->dateTimeConstraint = new DateTimeConstraint($em);
+
+       $this->periods = [
+           'dateStart' => '2021-12-20',
+           'dateEnd' => '2022-08-31'
+       ];
+   }
+
+   /**
+    * @see DateTimeConstraint::presentConstraint()
+    */
+   public function testPresentConstrain(){
+       $constraintExpected = [
+           'start' => [
+               '2022-08-31' => 4
+           ],
+           'end' => [
+               '2021-12-20' => 8,
+               'NULL' => 0
+           ]
+       ];
+       $this->assertEquals($constraintExpected, $this->invokeMethod($this->dateTimeConstraint, 'presentConstraint', [$this->periods]));
+   }
+
+    /**
+     * @see DateTimeConstraint::pastConstraint()
+     */
+    public function testPastConstrain(){
+        $constraintExpected = [
+            'end' => [
+                '2021-12-20' => 1
+            ]
+        ];
+        $this->assertEquals($constraintExpected, $this->invokeMethod($this->dateTimeConstraint, 'pastConstraint', [$this->periods]));
+    }
+
+    /**
+     * @see DateTimeConstraint::futurConstraint()
+     */
+    public function testFuturConstrain(){
+        $constraintExpected = [
+            'start' => [
+                '2022-08-31' => 5
+            ]
+        ];
+        $this->assertEquals($constraintExpected, $this->invokeMethod($this->dateTimeConstraint, 'futurConstraint', [$this->periods]));
+    }
+
+    /**
+     * @see DateTimeConstraint::clearNull()
+     */
+    public function testClearNull(){
+        $originalEndConstraint= [
+            'end' => [
+                'NULL' => 0
+            ]
+        ];
+        $endConstraintExpected = [];
+        $this->assertEquals($endConstraintExpected, $this->invokeMethod($this->dateTimeConstraint, 'clearNull', [$originalEndConstraint, 'end']));
+    }
+
+    /**
+     * @see DateTimeConstraint::filterConstraint()
+     */
+    public function testFilterConstraint(){
+        $originalStartConstraint= [
+            'start' => [
+                '2021-12-20' => [8, 1],
+                '2022-01-01' => [5]
+            ]
+        ];
+        $startConstraintExpected = [
+            '2022-01-01' => [5]
+        ];
+        $this->assertEquals($startConstraintExpected, $this->invokeMethod($this->dateTimeConstraint, 'filterConstraint', [$originalStartConstraint, 'start']));
+    }
+
+    /**
+     * @see DateTimeConstraint::cleanConstraints()
+     */
+    public function testCleanConstraints(){
+        $originalConstraint= [
+            'start' => [
+                '2022-08-31' => [5]
+            ],
+            'end' => [
+                '2021-12-20' => [8, 1],
+                'NULL' => [0]
+            ]
+        ];
+        $constraintExpected= [
+            'start' => [
+                '2022-08-31' => [5]
+            ],
+            'end' => []
+        ];
+        $this->assertEquals($constraintExpected, $this->invokeMethod($this->dateTimeConstraint, 'cleanConstraints', [$originalConstraint]));
+    }
+
+    /**
+     * @see DateTimeConstraint::addConstraint()
+     */
+    public function testAddConstraint(){
+        $originalConstraint = [
+            'start' => [],
+            'end' => []
+        ];
+        $presentConstraint = [
+            'start' => [
+                '2022-08-31' => 4
+            ],
+            'end' => [
+                '2021-12-20' => 8,
+                'NULL' => 0
+            ]
+        ];
+        $pastConstraint = [
+            'end' => [
+                '2021-12-20' => 1
+            ]
+        ];
+        $constraintAfterStartExpected = [
+            'start' => [
+                '2022-08-31' => [4]
+            ],
+            'end' => [
+                '2021-12-20' => [8],
+                'NULL' => [0]
+            ]
+        ];
+        $constraintAfterEndExpected = [
+            'start' => [
+                '2022-08-31' => [4]
+            ],
+            'end' => [
+                '2021-12-20' => [8, 1],
+                'NULL' => [0]
+            ]
+        ];
+        $this->assertEquals($constraintAfterStartExpected, $this->invokeMethod($this->dateTimeConstraint, 'addConstraint', [$originalConstraint, $presentConstraint]));
+        $this->assertEquals($constraintAfterEndExpected, $this->invokeMethod($this->dateTimeConstraint, 'addConstraint', [$constraintAfterStartExpected, $pastConstraint]));
+    }
+
+    /**
+     * @throws \ReflectionException
+     * @see DateTimeConstraint::getPeriods()
+     */
+    public function testGetPeriodsToday(){
+        $today = new \DateTime('now');
+
+        $organization = $this->getMockBuilder(Organization::class)->disableOriginalConstructor()->getMock();
+        $parameters = new Parameters();
+        $parameters->setMusicalDate(new \DateTime('2000-09-01'));
+        $organization->method('getParameters')->willReturn($parameters);
+
+        $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
+        $access->method('getOrganization')->willReturn($organization);
+
+        $activityYear = $today->format('Y');
+        if($today->format('m') < 9) $activityYear--;
+
+        $access->method('getActivityYear')->willReturn(intval($activityYear));
+
+        $periodExpected = ['dateStart' => $today->format('Y-m-d'), 'dateEnd' => '2022-08-31'];
+        $this->assertEquals($periodExpected, $this->invokeMethod($this->dateTimeConstraint, 'getPeriods', [$access]));
+    }
+
+    /**
+     * @throws \ReflectionException
+     * @see DateTimeConstraint::getPeriods()
+     */
+    public function testGetPeriodsNotToday(){
+        $organization = $this->getMockBuilder(Organization::class)->disableOriginalConstructor()->getMock();
+
+        $access = $this->getMockBuilder(Access::class)->disableOriginalConstructor()->getMock();
+        $access->method('getOrganization')->willReturn($organization);
+        $access->method('getActivityYear')->willReturn(2020);
+        $periodExpected = ['dateStart' => '2020-09-01', 'dateEnd' => '2021-08-31'];
+        $this->assertEquals($periodExpected, $this->invokeMethod($this->dateTimeConstraint, 'getPeriods', [$access]));
+    }
+
+    /**
+     * @throws \ReflectionException
+     * @see DateTimeConstraint::hasCustomPeriods()
+     */
+    public function testHasCustomPeriods(){
+        $historical = ['dateStart' => '2020-09-01', 'dateEnd' => '2021-08-31'];
+        $this->assertTrue($this->invokeMethod($this->dateTimeConstraint, 'hasCustomPeriods', [$historical]));
+    }
+
+    /**
+     * @throws \ReflectionException
+     * @see DateTimeConstraint::hasCustomPeriods()
+     */
+    public function testHasNotCustomPeriods(){
+        $historical = ['dateStart' => null, 'dateEnd' => null];
+        $this->assertFalse($this->invokeMethod($this->dateTimeConstraint, 'hasCustomPeriods', [$historical]));
+    }
+}

+ 16 - 0
tests/Service/Utils/StringsUtilsTest.php

@@ -0,0 +1,16 @@
+<?php
+namespace App\Tests\Service\Utils;
+
+use App\Service\Utils\StringsUtils;
+use PHPUnit\Framework\TestCase;
+
+class StringsUtilsTest extends TestCase
+{
+    /**
+     * @see StringsUtils::unquote()
+     */
+    public function testUnquote():void
+    {
+        $this->assertEquals("foo", StringsUtils::unquote("'foo"));
+    }
+}

+ 27 - 0
tests/TestToolsTrait.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace App\Tests;
+
+/**
+ * Classe de base pour les tests unitaires nécessitant
+ * un accès au kernel et à la base de données de test
+ *
+ */
+trait TestToolsTrait
+{
+   /**
+     * Exécute une méthode quelque soit son niveau de visibilité
+     * @param $object
+     * @param $methodName
+     * @param array $parameters
+     * @return mixed
+     * @throws \ReflectionException
+     */
+    protected function invokeMethod(&$object, $methodName, array $parameters = array())
+    {
+        $reflection = new \ReflectionClass(get_class($object));
+        $method = $reflection->getMethod($methodName);
+        $method->setAccessible(true);
+        return $method->invokeArgs($object, $parameters);
+    }
+}

Някои файлове не бяха показани, защото твърде много файлове са промени