Browse Source

Merge branch 'develop' into feature/structure_cmf_licence_page

Conflicts:
	.env
	composer.lock
	config/bundles.php
	src/Entity/Core/File.php
	src/Entity/Organization/Organization.php
	symfony.lock
Olivier Massot 3 years ago
parent
commit
bc3887f07b
51 changed files with 1570 additions and 240 deletions
  1. 4 0
      .env
  2. 5 1
      .env.preprod
  3. 4 0
      .env.prod
  4. 2 0
      composer.json
  5. 291 184
      composer.lock
  6. 2 0
      config/bundles.php
  7. 1 0
      config/opentalent/enum.yaml
  8. 4 0
      config/packages/dev/debug.yaml
  9. 19 0
      config/packages/dev/monolog.yaml
  10. 8 0
      config/packages/prod/deprecations.yaml
  11. 17 0
      config/packages/prod/monolog.yaml
  12. 12 0
      config/packages/test/monolog.yaml
  13. 8 0
      config/services.yaml
  14. 15 0
      src/Annotation/OrganizationDefaultValue.php
  15. 5 1
      src/Doctrine/Access/Extensions/DateTimeConstraintExtension.php
  16. 47 0
      src/Doctrine/Network/CurrentNetworkOrganizationExtension.php
  17. 47 0
      src/Doctrine/Organization/CurrentOrganizationArticleExtension.php
  18. 6 6
      src/Entity/Access/Access.php
  19. 59 0
      src/Entity/Billing/BillingSetting.php
  20. 11 3
      src/Entity/Core/BankAccount.php
  21. 14 2
      src/Entity/Core/ContactPoint.php
  22. 82 17
      src/Entity/Core/File.php
  23. 9 0
      src/Entity/Network/Network.php
  24. 46 5
      src/Entity/Network/NetworkOrganization.php
  25. 112 3
      src/Entity/Organization/Organization.php
  26. 4 1
      src/Entity/Organization/OrganizationAddressPostal.php
  27. 95 0
      src/Entity/Organization/OrganizationArticle.php
  28. 4 1
      src/Entity/Organization/Parameters.php
  29. 113 0
      src/Entity/Organization/TypeOfPractice.php
  30. 2 1
      src/Entity/Person/Person.php
  31. 18 0
      src/Enum/Cotisation/CategoryTypeOfPracticeEnum.php
  32. 51 0
      src/Enum/Cotisation/TypeOfPracticeEnum.php
  33. 17 0
      src/EventListener/Helper.php
  34. 68 0
      src/EventListener/Organization/OrganizationChangedSubscriber.php
  35. 16 0
      src/Repository/Billing/BillingSettingRepository.php
  36. 38 0
      src/Repository/Core/ContactPointRepository.php
  37. 18 0
      src/Repository/Organization/OrganizationAddressPostalRepository.php
  38. 18 0
      src/Repository/Organization/OrganizationArticleRepository.php
  39. 18 0
      src/Repository/Organization/TypeOfPracticeRepository.php
  40. 2 1
      src/Security/Voter/BankAccountVoter.php
  41. 2 2
      src/Security/Voter/ContactPointVoter.php
  42. 63 0
      src/Serializer/DefaultNormalizer.php
  43. 1 1
      src/Service/Dolibarr/DolibarrService.php
  44. 33 0
      src/Service/Utils/EntityUtils.php
  45. 3 3
      src/Service/Utils/Reflection.php
  46. 17 0
      src/Validator/Core/ContactPoint.php
  47. 42 0
      src/Validator/Core/ContactPointValidator.php
  48. 17 0
      src/Validator/Organization/OrganizationAddressPostal.php
  49. 37 0
      src/Validator/Organization/OrganizationAddressPostalValidator.php
  50. 34 7
      symfony.lock
  51. 9 1
      tests/Service/Utils/DateTimeConstraintTest.php

+ 4 - 0
.env

@@ -66,3 +66,7 @@ MESSENGER_TRANSPORT_DSN=doctrine://default
 # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
 # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
 ###< symfony/messenger ###
+
+###> AdminAssos configuration ###
+#DATABASE_ADMINASSOS_URL=mysql://root:mysql660@db:3306/adminassos?serverVersion=5.7
+###< AdminAssos configuration ###

+ 5 - 1
.env.preprod

@@ -9,7 +9,7 @@ APP_SECRET=6a76497c8658bb23e2236f97a2627df3
 ###> doctrine/doctrine-bundle ###
 # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
 # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
-DATABASE_URL=mysql://root:mysql2iopenservice369566@preprod.2iopenservice.com:3306/opentalent?serverVersion=5.7
+DATABASE_URL=mysql://root:mysql2iopenservice369566@preprod:3306/opentalent?serverVersion=5.7
 ###< doctrine/doctrine-bundle ###
 
 ###> nelmio/cors-bundle ###
@@ -22,3 +22,7 @@ BLACKFIRE_CLIENT_TOKEN=8cfbeb263d044da9678dc2612531504da3790c308da7448e35724a5da
 BLACKFIRE_SERVER_ID=1171e53b-459b-41da-a292-80ff68cee8c2
 BLACKFIRE_SERVER_TOKEN=dbd1cfbea015fe83cccfc189a36ca3c16f3a1b43b94f50032a15e41e53548e8b
 ###< BlackFire configuration ###
+
+###> AdminAssos configuration ###
+DATABASE_ADMINASSOS_URL=mysql://root:mysql660@preprod:3306/adminassos?serverVersion=5.7
+###< AdminAssos configuration ###

+ 4 - 0
.env.prod

@@ -22,3 +22,7 @@ BLACKFIRE_CLIENT_TOKEN=8cfbeb263d044da9678dc2612531504da3790c308da7448e35724a5da
 BLACKFIRE_SERVER_ID=1171e53b-459b-41da-a292-80ff68cee8c2
 BLACKFIRE_SERVER_TOKEN=dbd1cfbea015fe83cccfc189a36ca3c16f3a1b43b94f50032a15e41e53548e8b
 ###< BlackFire configuration ###
+
+###> AdminAssos configuration ###
+DATABASE_ADMINASSOS_URL=mysql://root:mysql660@prod-back:3306/adminassos?serverVersion=5.7
+###< AdminAssos configuration ###

+ 2 - 0
composer.json

@@ -36,6 +36,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.*",
@@ -48,6 +49,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",

File diff suppressed because it is too large
+ 291 - 184
composer.lock


+ 2 - 0
config/bundles.php

@@ -16,4 +16,6 @@ return [
     FOS\ElasticaBundle\FOSElasticaBundle::class => ['all' => true],
     Knp\Bundle\SnappyBundle\KnpSnappyBundle::class => ['all' => true],
     Knp\Bundle\GaufretteBundle\KnpGaufretteBundle::class => ['all' => true],
+    Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
+    Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
 ];

+ 1 - 0
config/opentalent/enum.yaml

@@ -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 - 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"]

+ 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

+ 8 - 0
config/services.yaml

@@ -55,6 +55,14 @@ services:
 
     #########################################
     ##  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' ]

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

+ 5 - 1
src/Doctrine/Access/Extensions/DateTimeConstraintExtension.php

@@ -14,7 +14,11 @@ class DateTimeConstraintExtension implements AccessExtensionInterface {
     }
     public function support(string $name): bool
     {
-        return $this->requestStack->getMainRequest()->get('_time_constraint', true) == true;
+        return
+            (
+                $this->requestStack->getMainRequest()->isMethod('GET') &&
+                $this->requestStack->getMainRequest()->get('_time_constraint', true) == true
+            );
     }
 
     public function addWhere(QueryBuilder $queryBuilder)

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

+ 6 - 6
src/Entity/Access/Access.php

@@ -505,19 +505,19 @@ class Access implements UserInterface
         return $this->person->getUsername();
     }
 
-    public function getPassword()
+    public function getPassword(): ?string
     {
-        // TODO: Implement getPassword() method.
+        return null;
     }
 
-    public function getSalt()
+    public function getSalt(): ?string
     {
-        // TODO: Implement getSalt() method.
+        return null;
     }
 
-    public function getUsername()
+    public function getUsername(): ?string
     {
-        // TODO: Implement getUsername() method.
+        return null;
     }
 
     public function eraseCredentials()

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

+ 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 - 2
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]
@@ -69,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()
@@ -215,6 +221,7 @@ class ContactPoint
     {
         if (!$this->organization->contains($organization)) {
             $this->organization[] = $organization;
+            $organization->addContactPoint($this);
         }
 
         return $this;
@@ -222,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;
     }
@@ -236,6 +245,7 @@ class ContactPoint
     {
         if (!$this->person->contains($person)) {
             $this->person[] = $person;
+            $person->addContactPoint($this);
         }
 
         return $this;
@@ -243,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;
     }

+ 82 - 17
src/Entity/Core/File.php

@@ -3,6 +3,7 @@ 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;
@@ -12,6 +13,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
 {
@@ -74,7 +82,7 @@ class File
      * Configuration particulière associée au fichier (exemple: image recadrée)
      * @var string|null
      */
-    #[ORM\Column(type: 'text', nullable: true)]
+    #[ORM\Column(type: 'text', length: 255, nullable: true)]
     private ?string $config;
 
     /**
@@ -157,9 +165,17 @@ class File
     #[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
@@ -244,6 +260,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;
@@ -287,22 +314,6 @@ class File
         $this->visibility = $visibility;
     }
 
-    /**
-     * @return string|null
-     */
-    public function getConfig(): ?string
-    {
-        return $this->config;
-    }
-
-    /**
-     * @param string|null $config
-     */
-    public function setConfig(?string $config): void
-    {
-        $this->config = $config;
-    }
-
     /**
      * @return string
      */
@@ -430,4 +441,58 @@ class File
     {
         $this->updatedBy = $updatedBy;
     }
+
+    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;
+    }
 }

+ 112 - 3
src/Entity/Organization/Organization.php

@@ -5,6 +5,7 @@ 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;
@@ -37,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)]
@@ -55,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)]
@@ -64,8 +66,8 @@ class Organization
     #[ORM\JoinColumn(nullable: false)]
     private Parameters $parameters;
 
-    #[ORM\OneToOne(cascade: ['persist', 'remove'])]
-    private File $logo;
+    #[ORM\OneToOne(mappedBy: 'organization', cascade: ['persist', 'remove'], orphanRemoval: true)]
+    private BillingSetting $billingSetting;
 
     #[ORM\Column(length: 255, nullable: true)]
     private ?string $description = null;
@@ -100,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;
 
@@ -168,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;
 
@@ -186,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
@@ -348,6 +371,18 @@ class Organization
         $this->logo = $logo;
     }
 
+    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;
@@ -480,6 +515,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;
@@ -732,6 +779,41 @@ class Organization
         return $this;
     }
 
+    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;
@@ -845,4 +927,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;
+    }
 }

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

@@ -4,12 +4,13 @@ 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;
-
 use Symfony\Component\Serializer\Annotation\Groups;
+use App\Validator\Organization as OpentalentAssert;
 
 #[ApiResource(
     collectionOperations: [
@@ -30,6 +31,8 @@ use Symfony\Component\Serializer\Annotation\Groups;
     ],
 )]
 #[ORM\Entity(repositoryClass: OrganizationAddressPostalRepository::class)]
+#[OrganizationDefaultValue(fieldName: "organization")]
+#[OpentalentAssert\OrganizationAddressPostal]
 class OrganizationAddressPostal
 {
     #[ORM\Id]

+ 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

@@ -9,7 +9,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;
+    }
+}

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

@@ -122,8 +122,9 @@ class Person implements UserInterface
         return $this;
     }
 
-    public function getSalt()
+    public function getSalt(): ?string
     {
+        return null;
         // not needed when using the "bcrypt" algorithm in security.yaml
     }
 

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

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

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

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

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

+ 1 - 1
src/Service/Dolibarr/DolibarrService.php

@@ -41,7 +41,7 @@ class DolibarrService extends ApiRequestService
         try {
             return $this->getJsonContent(
                 "contracts",
-                ["limit" => "1", "sqlfilters" => "statut=1", "thirdparty_ids=" . $socId => null]
+                ["limit" => "1", "sqlfilters" => "statut=1", "thirdparty_ids" => $socId]
             )[0];
         } catch (NotFoundHttpException) {
             // /!\ The dolibarr API will return a 404 error if no contract is found...

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

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

+ 34 - 7
symfony.lock

@@ -147,10 +147,7 @@
             "branch": "master",
             "version": "1.5",
             "ref": "c81bdcf4a9d4e7b1959071457f9608631865d381"
-        },
-        "files": [
-            "config/packages/knp_snappy.yaml"
-        ]
+        }
     },
     "laminas/laminas-code": {
         "version": "4.4.2"
@@ -173,6 +170,9 @@
             "config/packages/lexik_jwt_authentication.yaml"
         ]
     },
+    "monolog/monolog": {
+        "version": "2.3.5"
+    },
     "myclabs/php-enum": {
         "version": "1.7.7"
     },
@@ -284,6 +284,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"
     },
@@ -385,6 +397,24 @@
             "config/packages/messenger.yaml"
         ]
     },
+    "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"
     },
@@ -433,9 +463,6 @@
     "symfony/process": {
         "version": "v5.3.14"
     },
-    "symfony/profiler-pack": {
-        "version": "v1.0.5"
-    },
     "symfony/property-access": {
         "version": "v5.1.7"
     },

+ 9 - 1
tests/Service/Utils/DateTimeConstraintTest.php

@@ -3,6 +3,7 @@ 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;
@@ -169,10 +170,17 @@ class DateTimeConstraintTest extends TestCase
         $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);
-        $access->method('getActivityYear')->willReturn(intval($today->format('Y')));
+
+        $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]));

Some files were not shown because too many files changed in this diff