Jelajahi Sumber

Merge branch 'feature/V8-3988-suppression-orgas' into feature/V8-6489-dvelopper-loutil-de-cration-des-

Olivier Massot 10 bulan lalu
induk
melakukan
cc50272fd5
40 mengubah file dengan 1038 tambahan dan 161 penghapusan
  1. 2 2
      config/packages/knp_gaufrette.yaml
  2. 2 0
      config/services.yaml
  3. 110 0
      src/ApiResources/Organization/OrganizationDeletionRequest.php
  4. 45 45
      src/Entity/Access/Access.php
  5. 32 32
      src/Entity/Organization/Organization.php
  6. 8 8
      src/Entity/Person/Person.php
  7. 1 0
      src/Enum/Organization/OrganizationIdsEnum.php
  8. 1 1
      src/Message/Handler/ExportHandler.php
  9. 1 1
      src/Message/Handler/MailerHandler.php
  10. 2 2
      src/Message/Handler/OrganizationCreationHandler.php
  11. 60 0
      src/Message/Handler/OrganizationDeletionHandler.php
  12. 3 3
      src/Message/Handler/Typo3/Typo3DeleteHandler.php
  13. 3 3
      src/Message/Handler/Typo3/Typo3UndeleteHandler.php
  14. 3 3
      src/Message/Handler/Typo3/Typo3UpdateHandler.php
  15. 1 1
      src/Message/Message/Export.php
  16. 1 1
      src/Message/Message/MailerCommand.php
  17. 2 2
      src/Message/Message/OrganizationCreation.php
  18. 29 0
      src/Message/Message/OrganizationDeletion.php
  19. 2 2
      src/Message/Message/Typo3/Typo3Delete.php
  20. 2 2
      src/Message/Message/Typo3/Typo3Undelete.php
  21. 2 2
      src/Message/Message/Typo3/Typo3Update.php
  22. 20 0
      src/Repository/Core/FileRepository.php
  23. 23 5
      src/Service/Dolibarr/DolibarrApiService.php
  24. 37 1
      src/Service/File/FileManager.php
  25. 24 0
      src/Service/File/Storage/ApiLegacyStorage.php
  26. 4 0
      src/Service/File/Storage/FileStorageInterface.php
  27. 49 7
      src/Service/File/Storage/LocalStorage.php
  28. 7 7
      src/Service/OnChange/Organization/OnParametersChange.php
  29. 154 0
      src/Service/Organization/OrganizationFactory.php
  30. 5 0
      src/Service/ServiceIterator/StorageIterator.php
  31. 3 3
      src/Service/Typo3/SubdomainService.php
  32. 1 1
      src/Service/Utils/Path.php
  33. 23 0
      src/Service/Utils/SecurityUtils.php
  34. 1 1
      src/State/Processor/Export/LicenceCmf/ExportRequestProcessor.php
  35. 12 7
      src/State/Processor/Organization/OrganizationCreationRequestProcessor.php
  36. 55 0
      src/State/Processor/Organization/OrganizationDeletionRequestProcessor.php
  37. 55 4
      tests/Unit/Service/Dolibarr/DolibarrApiServiceTest.php
  38. 11 11
      tests/Unit/Service/OnChange/Organization/OnParametersChangeTest.php
  39. 238 0
      tests/Unit/Service/Organization/OrganizationFactoryTest.php
  40. 4 4
      tests/Unit/Service/Typo3/SubdomainServiceTest.php

+ 2 - 2
config/packages/knp_gaufrette.yaml

@@ -4,10 +4,10 @@ knp_gaufrette:
   adapters:
     storage:
       local:
-        directory: '%kernel.project_dir%/storage'
+        directory: '%kernel.project_dir%/var/files/storage'
         create: true
   filesystems:
     storage:
       adapter: storage
 
-  stream_wrapper: ~
+  stream_wrapper: ~

+ 2 - 0
config/services.yaml

@@ -23,6 +23,8 @@ services:
             $opentalentNoReplyEmailAddress: 'noreply@opentalent.fr'
             $legacyBaseUrl: '%env(PUBLIC_API_LEG_BASE_URL)%'
             $baseUrl: '%env(PUBLIC_API_BASE_URL)%'
+            $opentalentMailReport: 'mail.report@opentalent.fr'
+            $fileStorageDir: '%kernel.project_dir%/var/files/storage'
 
     # makes classes in src/ available to be used as services
     # this creates a service per class whose id is the fully-qualified class name

+ 110 - 0
src/ApiResources/Organization/OrganizationDeletionRequest.php

@@ -0,0 +1,110 @@
+<?php
+
+namespace App\ApiResources\Organization;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Post;
+use App\State\Processor\Organization\OrganizationDeletionRequestProcessor;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * Requête de création d'une nouvelle organisation
+ */
+#[ApiResource(
+    operations: [
+        new Post(
+            uriTemplate: '/internal/organization/delete',
+        ),
+    ],
+    processor: OrganizationDeletionRequestProcessor::class
+)]
+class OrganizationDeletionRequest
+{
+    public const STATUS_PENDING = 'pending';
+    public const STATUS_OK = 'ok';
+    public const STATUS_OK_WITH_ERRORS = 'ok with errors';
+
+    /**
+     * Id 'bidon' ajouté par défaut pour permettre la construction
+     * de l'IRI par api platform.
+     */
+    #[ApiProperty(identifier: true)]
+    private int $id = 0;
+
+    private int $organizationId;
+
+    /**
+     * A quelle adresse email notifier la création de l'organisation, ou d'éventuelles erreurs ?
+     * @var string|null
+     */
+    #[Assert\Email(message: 'The email {{ value }} is not a valid email.')]
+    private ?string $sendConfirmationEmailAt = null;
+
+    /**
+     * Statut de l'opération
+     * @var string
+     */
+    private string $status = self::STATUS_PENDING;
+
+    /**
+     * For testing purposes only
+     * @var bool
+     */
+    private bool $async = true;
+
+    public function getId(): int
+    {
+        return $this->id;
+    }
+
+    public function setId(int $id): self
+    {
+        $this->id = $id;
+        return $this;
+    }
+
+    public function getOrganizationId(): int
+    {
+        return $this->organizationId;
+    }
+
+    public function setOrganizationId(int $organizationId): self
+    {
+        $this->organizationId = $organizationId;
+        return $this;
+    }
+
+    public function getSendConfirmationEmailAt(): ?string
+    {
+        return $this->sendConfirmationEmailAt;
+    }
+
+    public function setSendConfirmationEmailAt(?string $sendConfirmationEmailAt): self
+    {
+        $this->sendConfirmationEmailAt = $sendConfirmationEmailAt;
+        return $this;
+    }
+
+    public function getStatus(): string
+    {
+        return $this->status;
+    }
+
+    public function setStatus(string $status): self
+    {
+        $this->status = $status;
+        return $this;
+    }
+
+    public function isAsync(): bool
+    {
+        return $this->async;
+    }
+
+    public function setAsync(bool $async): self
+    {
+        $this->async = $async;
+        return $this;
+    }
+}

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

@@ -123,22 +123,22 @@ class Access implements UserInterface, PasswordAuthenticatedUserInterface
     #[ORM\OneToOne(mappedBy: 'access', cascade: ['persist'], orphanRemoval: true)]
     private AccessBilling $accessBilling;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: PersonActivity::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: PersonActivity::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $personActivity;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: OrganizationFunction::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: OrganizationFunction::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $organizationFunction;
 
-    #[ORM\OneToMany(mappedBy: 'licensee', targetEntity: OrganizationLicence::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'licensee', targetEntity: OrganizationLicence::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $organizationLicences;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: PersonalizedList::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: PersonalizedList::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $personalizedLists;
 
-    #[ORM\OneToMany(mappedBy: 'recipientAccess', targetEntity: Notification::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'recipientAccess', targetEntity: Notification::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $notifications;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: NotificationUser::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: NotificationUser::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $notificationUsers;
 
     #[ORM\ManyToMany(targetEntity: Access::class, mappedBy: 'children', cascade: ['persist'])]
@@ -150,13 +150,13 @@ class Access implements UserInterface, PasswordAuthenticatedUserInterface
     #[ORM\InverseJoinColumn(name: 'children_id', referencedColumnName: 'id')]
     private Collection $children;
 
-    #[ORM\OneToMany(mappedBy: 'accessPayer', targetEntity: AccessPayer::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'accessPayer', targetEntity: AccessPayer::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $billingPayers;
 
-    #[ORM\OneToMany(mappedBy: 'accessReceiver', targetEntity: AccessPayer::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'accessReceiver', targetEntity: AccessPayer::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $billingReceivers;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: AccessIntangible::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: AccessIntangible::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $accessIntangibles;
 
     #[ORM\ManyToOne(inversedBy: 'publicationDirectors')]
@@ -167,22 +167,22 @@ class Access implements UserInterface, PasswordAuthenticatedUserInterface
     #[ORM\JoinColumn(referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
     private ?EducationNotationConfig $educationNotationConfig;
 
-    #[ORM\OneToMany(mappedBy: 'company', targetEntity: CompanyPerson::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'company', targetEntity: CompanyPerson::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $companyPersonAccesses;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: CompanyPerson::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: CompanyPerson::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $companyPersonCompany;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: EducationStudent::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: EducationStudent::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $educationStudent;
 
-    #[ORM\ManyToMany(targetEntity: EducationStudent::class, mappedBy: 'teachers', cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\ManyToMany(targetEntity: EducationStudent::class, mappedBy: 'teachers', cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $educationStudentByTeacher;
 
-    #[ORM\OneToMany(mappedBy: 'teacher', targetEntity: EducationTeacher::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'teacher', targetEntity: EducationTeacher::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $educationTeachers;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: PersonHoliday::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: PersonHoliday::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $holidays;
 
     #[ORM\ManyToMany(targetEntity: Course::class, mappedBy: 'students', cascade: ['persist'])]
@@ -207,97 +207,97 @@ class Access implements UserInterface, PasswordAuthenticatedUserInterface
     #[ORM\Column]
     private bool $ielEnabled = false;
 
-    #[ORM\OneToMany(mappedBy: 'educationalProjectPayer', targetEntity: EducationalProjectPayer::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'educationalProjectPayer', targetEntity: EducationalProjectPayer::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $billingEducationalProjectPayers;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: Bill::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: Bill::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $bills;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: BillLine::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: BillLine::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $billLines;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: BillCredit::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: BillCredit::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $billCredits;
 
-    #[ORM\OneToMany(mappedBy: 'silentPartner', targetEntity: EducationalProject::class)]
+    #[ORM\OneToMany(mappedBy: 'silentPartner', targetEntity: EducationalProject::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $silentPartners;
 
-    #[ORM\OneToMany(mappedBy: 'operationalPartner', targetEntity: EducationalProject::class)]
+    #[ORM\OneToMany(mappedBy: 'operationalPartner', targetEntity: EducationalProject::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $operationalPartners;
 
-    #[ORM\OneToMany(mappedBy: 'guest', targetEntity: EventUser::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'guest', targetEntity: EventUser::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $eventUsers;
 
-    #[ORM\OneToMany(mappedBy: 'student', targetEntity: ExamenConvocation::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'student', targetEntity: ExamenConvocation::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $examenConvocations;
 
-    #[ORM\OneToMany(mappedBy: 'provider', targetEntity: EquipmentRepair::class)]
+    #[ORM\OneToMany(mappedBy: 'provider', targetEntity: EquipmentRepair::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $equipmentRepairProviders;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: Attendance::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: Attendance::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $attendances;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: AttendanceBooking::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: AttendanceBooking::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $attendanceBookings;
 
-    #[ORM\OneToMany(mappedBy: 'replacement', targetEntity: Attendance::class)]
+    #[ORM\OneToMany(mappedBy: 'replacement', targetEntity: Attendance::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $attendanceReplacements;
 
-    #[ORM\OneToMany(mappedBy: 'provider', targetEntity: RoomRepair::class)]
+    #[ORM\OneToMany(mappedBy: 'provider', targetEntity: RoomRepair::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $roomRepairProviders;
 
-    #[ORM\OneToMany(mappedBy: 'provider', targetEntity: PlaceRepair::class)]
+    #[ORM\OneToMany(mappedBy: 'provider', targetEntity: PlaceRepair::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $placeRepairProviders;
 
-    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Email::class)]
+    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Email::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $emails;
 
-    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Mail::class)]
+    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Mail::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $mails;
 
-    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Sms::class)]
+    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Sms::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $sms;
 
     #[ORM\ManyToMany(targetEntity: Jury::class, mappedBy: 'members', orphanRemoval: true)]
     private Collection $juryMembers;
 
-    #[ORM\OneToMany(mappedBy: 'contactPerson', targetEntity: Organization::class)]
+    #[ORM\OneToMany(mappedBy: 'contactPerson', targetEntity: Organization::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $organizationContacts;
 
-    #[ORM\OneToMany(mappedBy: 'member', targetEntity: CommissionMember::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'member', targetEntity: CommissionMember::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $commissionMembers;
 
-    #[ORM\OneToMany(mappedBy: 'supplier', targetEntity: Equipment::class)]
+    #[ORM\OneToMany(mappedBy: 'supplier', targetEntity: Equipment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $equipmentSuppliers;
 
-    #[ORM\OneToMany(mappedBy: 'controlManager', targetEntity: Equipment::class)]
+    #[ORM\OneToMany(mappedBy: 'controlManager', targetEntity: Equipment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $equipmentControlManagers;
 
-    #[ORM\OneToMany(mappedBy: 'editor', targetEntity: Equipment::class)]
+    #[ORM\OneToMany(mappedBy: 'editor', targetEntity: Equipment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $equipmentEditors;
 
-    #[ORM\OneToMany(mappedBy: 'borrower', targetEntity: EquipmentLoan::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'borrower', targetEntity: EquipmentLoan::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $equipmentLoans;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: Equipment::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: Equipment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $equipments;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: AccessFictionalIntangible::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: AccessFictionalIntangible::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $accessFictionalIntangibles;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: Donor::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: Donor::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $donors;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: AccessReward::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: AccessReward::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $accessRewards;
 
-    #[ORM\OneToMany(mappedBy: 'access', targetEntity: OrganizationResponsability::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'access', targetEntity: OrganizationResponsability::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $organizationResponsabilities;
 
-    #[ORM\OneToMany(mappedBy: 'accessOriginal', targetEntity: AccessWish::class, cascade: ['persist', 'remove'], fetch: 'EAGER', orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'accessOriginal', targetEntity: AccessWish::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $accessWishes;
 
-    #[ORM\OneToMany(mappedBy: 'student', targetEntity: WorkByUser::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'student', targetEntity: WorkByUser::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $workByUsers;
 
     #[ORM\ManyToMany(targetEntity: Tagg::class, inversedBy: 'accesses', cascade: ['persist'])]

+ 32 - 32
src/Entity/Organization/Organization.php

@@ -86,16 +86,16 @@ class Organization
     #[ORM\OneToOne(mappedBy: 'organization', cascade: ['persist', 'remove'])]
     private Settings $settings;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Access::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Access::class, cascade: ['persist'])]
     private Collection $accesses;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: NetworkOrganization::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: NetworkOrganization::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $networkOrganizations;
 
-    #[ORM\OneToMany(mappedBy: 'parent', targetEntity: NetworkOrganization::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'parent', targetEntity: NetworkOrganization::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $networkOrganizationChildren;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: EducationNotationConfig::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: EducationNotationConfig::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $educationNotationConfigs;
 
     #[ORM\OneToOne(inversedBy: 'organization', targetEntity: Parameters::class, cascade: ['persist'])]
@@ -230,92 +230,92 @@ class Organization
     #[ORM\InverseJoinColumn(name: 'bankAccount_id', referencedColumnName: 'id')]
     private Collection $bankAccounts;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: OrganizationAddressPostal::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: OrganizationAddressPostal::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $organizationAddressPostals;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: OrganizationLicence::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: OrganizationLicence::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $organizationLicences;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: OrganizationArticle::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: OrganizationArticle::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $organizationArticles;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Cycle::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Cycle::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $cycles;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: EducationTiming::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: EducationTiming::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $educationTimings;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Subdomain::class, cascade: ['persist'])]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Subdomain::class, cascade: ['persist', 'remove'])]
     private Collection $subdomains;
 
     #[ORM\ManyToOne(inversedBy: 'organizationContacts')]
     #[ORM\JoinColumn(referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
     private ?Access $contactPerson;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: OrganizationHoliday::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: OrganizationHoliday::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $holidays;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Course::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Course::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $courses;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: EducationalProject::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: EducationalProject::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $educationalProjects;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Event::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Event::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $events;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Examen::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Examen::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $examens;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: CriteriaNotation::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: CriteriaNotation::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $critereNotations;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: EducationCategory::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: EducationCategory::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $educationCategories;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: PeriodNotation::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: PeriodNotation::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $periodNotations;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: File::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: File::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $files;
 
-    #[ORM\OneToMany(mappedBy: 'recipientOrganization', targetEntity: Notification::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'recipientOrganization', targetEntity: Notification::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $notifications;
 
-    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Email::class)]
+    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Email::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $emails;
 
-    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Mail::class)]
+    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Mail::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $mails;
 
-    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Sms::class)]
+    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Sms::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $sms;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Activity::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Activity::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $activities;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Jury::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Jury::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $juries;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Commission::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Commission::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $commissions;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Place::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Place::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $places;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Attendance::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Attendance::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $attendances;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Equipment::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Equipment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $equipments;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Intangible::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Intangible::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $intangibles;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Donor::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Donor::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $donors;
 
-    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Reward::class, orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'organization', targetEntity: Reward::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $rewards;
 
     //    #[ORM\OneToOne()]

+ 8 - 8
src/Entity/Person/Person.php

@@ -70,7 +70,7 @@ class Person implements UserInterface, PasswordAuthenticatedUserInterface
     #[ORM\InverseJoinColumn(name: 'bankAccount_id', referencedColumnName: 'id')]
     private Collection $bankAccount;
 
-    #[ORM\OneToMany(mappedBy: 'person', targetEntity: PersonAddressPostal::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'person', targetEntity: PersonAddressPostal::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     #[Groups('access_address')]
     private Collection $personAddressPostal;
 
@@ -93,23 +93,23 @@ class Person implements UserInterface, PasswordAuthenticatedUserInterface
     #[ORM\OneToMany(mappedBy: 'person', targetEntity: File::class, orphanRemoval: true)]
     private Collection $files;
 
-    #[ORM\OneToMany(mappedBy: 'person', targetEntity: DisciplineOtherEstablishment::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'person', targetEntity: DisciplineOtherEstablishment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $disciplineotherestablishments;
 
-    #[ORM\OneToMany(mappedBy: 'person', targetEntity: Qualification::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'person', targetEntity: Qualification::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $qualifications;
 
-    #[ORM\OneToMany(mappedBy: 'person', targetEntity: SchoolingInEstablishment::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'person', targetEntity: SchoolingInEstablishment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $schoolingEstablisments;
 
-    #[ORM\OneToMany(mappedBy: 'person', targetEntity: TeacherSchoolingHistory::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'person', targetEntity: TeacherSchoolingHistory::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $teacherSchoolingHistories;
 
-    #[ORM\ManyToMany(targetEntity: File::class, mappedBy: 'accessPersons', cascade: ['persist'])]
+    #[ORM\ManyToMany(targetEntity: File::class, mappedBy: 'accessPersons', cascade: ['persist', 'remove'])]
     #[ORM\OrderBy(['id' => 'DESC'])]
     private Collection $personFiles;
 
-    #[ORM\OneToMany(mappedBy: 'personOwner', targetEntity: DocumentWish::class, cascade: ['persist'], orphanRemoval: true)]
+    #[ORM\OneToMany(mappedBy: 'personOwner', targetEntity: DocumentWish::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
     private Collection $documentWishes;
 
     /** @var array<string, string> */
@@ -379,7 +379,7 @@ class Person implements UserInterface, PasswordAuthenticatedUserInterface
     /**
      * @return Collection<int, Access>
      */
-    public function getAccess(): Collection
+    public function getAccesses(): Collection
     {
         return $this->access;
     }

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

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

+ 1 - 1
src/Message/Handler/ExportHandler.php

@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace App\Message\Handler;
 
-use App\Message\Command\Export;
+use App\Message\Message\Export;
 use App\Repository\Access\AccessRepository;
 use App\Service\MercureHub;
 use App\Service\Notifier;

+ 1 - 1
src/Message/Handler/MailerHandler.php

@@ -5,7 +5,7 @@ declare(strict_types=1);
 namespace App\Message\Handler;
 
 use App\Entity\Message\Email;
-use App\Message\Command\MailerCommand;
+use App\Message\Message\MailerCommand;
 use App\Repository\Access\AccessRepository;
 use App\Service\Mailer\Mailer;
 use App\Service\Notifier;

+ 2 - 2
src/Message/Handler/OrganizationCreationHandler.php

@@ -4,7 +4,7 @@ declare(strict_types=1);
 
 namespace App\Message\Handler;
 
-use App\Message\Command\OrganizationCreationCommand;
+use App\Message\Message\OrganizationCreation;
 use App\Service\Organization\OrganizationFactory;
 use Symfony\Component\Mailer\MailerInterface;
 use Symfony\Component\Messenger\Attribute\AsMessageHandler;
@@ -26,7 +26,7 @@ class OrganizationCreationHandler
      * @throws TransportExceptionInterface
      * @throws \Symfony\Component\Mailer\Exception\TransportExceptionInterface
      */
-    public function __invoke(OrganizationCreationCommand $organizationCreationCommand): void
+    public function __invoke(OrganizationCreation $organizationCreationCommand): void
     {
         $organizationCreationRequest = $organizationCreationCommand->getOrganizationCreationRequest();
         $mail = ['subject' => '', 'content' => ''];

+ 60 - 0
src/Message/Handler/OrganizationDeletionHandler.php

@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Message\Handler;
+
+use App\Message\Message\OrganizationCreation;
+use App\Message\Message\OrganizationDeletion;
+use App\Service\Organization\OrganizationFactory;
+use Symfony\Component\Mailer\MailerInterface;
+use Symfony\Component\Messenger\Attribute\AsMessageHandler;
+use Symfony\Component\Mime\Address;
+use Symfony\Component\Mime\Email as SymfonyEmail;
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
+use Throwable;
+
+#[AsMessageHandler(priority: 1)]
+class OrganizationDeletionHandler
+{
+    public function __construct(
+        private readonly OrganizationFactory $organizationFactory,
+        private readonly MailerInterface $symfonyMailer,
+        private readonly string $opentalentMailReport
+    ) {}
+
+    /**
+     * @throws Throwable
+     * @throws TransportExceptionInterface
+     * @throws \Symfony\Component\Mailer\Exception\TransportExceptionInterface
+     */
+    public function __invoke(OrganizationDeletion $organizationDeletionCommand): void
+    {
+        $organizationCreationRequest = $organizationDeletionCommand->getOrganizationDeletionRequest();
+        $mail = ['subject' => '', 'content' => ''];
+
+        try {
+            $this->organizationFactory->delete($organizationCreationRequest);
+
+            $mail['subject'] = 'Organization deleted';
+            $mail['content'] = 'The organization n° ' . $organizationCreationRequest->getOrganizationId() . ' has been deleted successfully.';
+
+        } catch (\Exception $e) {
+            $mail['subject'] = 'Organization deletion : an error occured';
+            $mail['content'] = 'An error occured while deleting the new organization : \n' . $e->getMessage();
+            throw $e;
+
+        } finally {
+            if ($organizationCreationRequest->getSendConfirmationEmailAt() !== null) {
+                $symfonyMail = (new SymfonyEmail())
+                    ->from($this->opentalentMailReport)
+                    ->replyTo($this->opentalentMailReport)
+                    ->returnPath(Address::create($this->opentalentMailReport))
+                    ->to($organizationCreationRequest->getSendConfirmationEmailAt())
+                    ->subject($mail['subject'])
+                    ->text($mail['content']);
+                $this->symfonyMailer->send($symfonyMail);
+            }
+        }
+    }
+}

+ 3 - 3
src/Message/Handler/Typo3/Typo3DeleteCommandHandler.php → src/Message/Handler/Typo3/Typo3DeleteHandler.php

@@ -2,12 +2,12 @@
 
 namespace App\Message\Handler\Typo3;
 
-use App\Message\Command\Typo3\Typo3DeleteCommand;
+use App\Message\Message\Typo3\Typo3Delete;
 use App\Service\Typo3\Typo3Service;
 use Symfony\Component\Messenger\Attribute\AsMessageHandler;
 
 #[AsMessageHandler(priority: 1)]
-class Typo3DeleteCommandHandler
+class Typo3DeleteHandler
 {
     public function __construct(
         private Typo3Service $typo3Service
@@ -17,7 +17,7 @@ class Typo3DeleteCommandHandler
     /**
      * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
      */
-    public function __invoke(Typo3DeleteCommand $command): void
+    public function __invoke(Typo3Delete $command): void
     {
         $this->typo3Service->deleteSite($command->getOrganizationId());
     }

+ 3 - 3
src/Message/Handler/Typo3/Typo3UndeleteCommandHandler.php → src/Message/Handler/Typo3/Typo3UndeleteHandler.php

@@ -2,12 +2,12 @@
 
 namespace App\Message\Handler\Typo3;
 
-use App\Message\Command\Typo3\Typo3UndeleteCommand;
+use App\Message\Message\Typo3\Typo3Undelete;
 use App\Service\Typo3\Typo3Service;
 use Symfony\Component\Messenger\Attribute\AsMessageHandler;
 
 #[AsMessageHandler(priority: 1)]
-class Typo3UndeleteCommandHandler
+class Typo3UndeleteHandler
 {
     public function __construct(
         private Typo3Service $typo3Service
@@ -17,7 +17,7 @@ class Typo3UndeleteCommandHandler
     /**
      * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
      */
-    public function __invoke(Typo3UndeleteCommand $command): void
+    public function __invoke(Typo3Undelete $command): void
     {
         $this->typo3Service->undeleteSite($command->getOrganizationId());
     }

+ 3 - 3
src/Message/Handler/Typo3/Typo3UpdateCommandHandler.php → src/Message/Handler/Typo3/Typo3UpdateHandler.php

@@ -2,12 +2,12 @@
 
 namespace App\Message\Handler\Typo3;
 
-use App\Message\Command\Typo3\Typo3UpdateCommand;
+use App\Message\Message\Typo3\Typo3Update;
 use App\Service\Typo3\Typo3Service;
 use Symfony\Component\Messenger\Attribute\AsMessageHandler;
 
 #[AsMessageHandler(priority: 1)]
-class Typo3UpdateCommandHandler
+class Typo3UpdateHandler
 {
     public function __construct(
         private Typo3Service $typo3Service
@@ -17,7 +17,7 @@ class Typo3UpdateCommandHandler
     /**
      * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
      */
-    public function __invoke(Typo3UpdateCommand $command): void
+    public function __invoke(Typo3Update $command): void
     {
         $this->typo3Service->updateSite($command->getOrganizationId());
     }

+ 1 - 1
src/Message/Command/Export.php → src/Message/Message/Export.php

@@ -2,7 +2,7 @@
 
 declare(strict_types=1);
 
-namespace App\Message\Command;
+namespace App\Message\Message;
 
 use App\ApiResources\Export\ExportRequest;
 

+ 1 - 1
src/Message/Command/MailerCommand.php → src/Message/Message/MailerCommand.php

@@ -2,7 +2,7 @@
 
 declare(strict_types=1);
 
-namespace App\Message\Command;
+namespace App\Message\Message;
 
 use App\Service\Mailer\Model\MailerModelInterface;
 

+ 2 - 2
src/Message/Command/OrganizationCreationCommand.php → src/Message/Message/OrganizationCreation.php

@@ -2,14 +2,14 @@
 
 declare(strict_types=1);
 
-namespace App\Message\Command;
+namespace App\Message\Message;
 
 use App\ApiResources\Organization\OrganizationCreationRequest;
 
 /**
  * Transmission d'une requête de création d'organisation au service dédié.
  */
-class OrganizationCreationCommand
+class OrganizationCreation
 {
     public function __construct(
         private OrganizationCreationRequest $organizationCreationRequest,

+ 29 - 0
src/Message/Message/OrganizationDeletion.php

@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Message\Message;
+
+use App\ApiResources\Organization\OrganizationDeletionRequest;
+
+/**
+ * Transmission d'une requête de création d'organisation au service dédié.
+ */
+class OrganizationDeletion
+{
+    public function __construct(
+        private OrganizationDeletionRequest $organizationDeletionRequest
+    ) {
+    }
+
+    public function getOrganizationDeletionRequest(): OrganizationDeletionRequest
+    {
+        return $this->organizationDeletionRequest;
+    }
+
+    public function setOrganizationDeletionRequest(OrganizationDeletionRequest $organizationDeletionRequest): self
+    {
+        $this->organizationDeletionRequest = $organizationDeletionRequest;
+        return $this;
+    }
+}

+ 2 - 2
src/Message/Command/Typo3/Typo3DeleteCommand.php → src/Message/Message/Typo3/Typo3Delete.php

@@ -2,12 +2,12 @@
 
 declare(strict_types=1);
 
-namespace App\Message\Command\Typo3;
+namespace App\Message\Message\Typo3;
 
 /**
  * Envoi d'une requête Delete à l'api Typo3.
  */
-class Typo3DeleteCommand
+class Typo3Delete
 {
     public function __construct(
         private int $organizationId

+ 2 - 2
src/Message/Command/Typo3/Typo3UndeleteCommand.php → src/Message/Message/Typo3/Typo3Undelete.php

@@ -2,12 +2,12 @@
 
 declare(strict_types=1);
 
-namespace App\Message\Command\Typo3;
+namespace App\Message\Message\Typo3;
 
 /**
  * Envoi d'une requête Undelete à l'api Typo3.
  */
-class Typo3UndeleteCommand
+class Typo3Undelete
 {
     public function __construct(
         private int $organizationId

+ 2 - 2
src/Message/Command/Typo3/Typo3UpdateCommand.php → src/Message/Message/Typo3/Typo3Update.php

@@ -2,12 +2,12 @@
 
 declare(strict_types=1);
 
-namespace App\Message\Command\Typo3;
+namespace App\Message\Message\Typo3;
 
 /**
  * Envoi d'une requête Update à l'api Typo3.
  */
-class Typo3UpdateCommand
+class Typo3Update
 {
     public function __construct(
         private int $organizationId

+ 20 - 0
src/Repository/Core/FileRepository.php

@@ -14,4 +14,24 @@ class FileRepository extends ServiceEntityRepository
     {
         parent::__construct($registry, File::class);
     }
+
+    public function deleteByOrganization(int $organizationId): void
+    {
+        $this->createQueryBuilder('f')
+            ->delete()
+            ->where('f.organization = :organizationId')
+            ->setParameter('organizationId', $organizationId)
+            ->getQuery()
+            ->execute();
+    }
+
+    public function deleteByPerson(int $personId): void
+    {
+        $this->createQueryBuilder('f')
+            ->delete()
+            ->where('f.person = :personId')
+            ->setParameter('personId', $personId)
+            ->getQuery()
+            ->execute();
+    }
 }

+ 23 - 5
src/Service/Dolibarr/DolibarrApiService.php

@@ -9,6 +9,7 @@ use App\Service\Rest\ApiRequestService;
 use JetBrains\PhpStorm\Pure;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Exception\HttpException;
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
 use Symfony\Contracts\HttpClient\HttpClientInterface;
 
 /**
@@ -183,9 +184,6 @@ class DolibarrApiService extends ApiRequestService
 
     /**
      * Créé une société dans la DB dolibarr, et retourne l'id de celle-ci.
-     *
-     * @param Organization $organization
-     * @return mixed
      */
     public function createSociety(Organization $organization, bool $client = false): mixed
     {
@@ -194,12 +192,32 @@ class DolibarrApiService extends ApiRequestService
             'client' => $client ? 1 : 2,
             'code_client' => -1,
             'import_key' => 'crm',
-            'array_options' => ['options_2iopen_organization_id' => $organization->getId()]
+            'array_options' => ['options_2iopen_organization_id' => $organization->getId()],
         ];
 
         /** @var Response $response */
-        $response = $this->post("/thirdparties", $body);
+        $response = $this->post('/thirdparties', $body);
 
         return json_decode($response->getContent(), true);
     }
+
+    /**
+     * Delete the organization from Dolibarr.
+     *
+     * @throws \JsonException
+     * @throws TransportExceptionInterface
+     */
+    public function switchSocietyToProspect(int $organizationId): void
+    {
+        $socId = $this->getSociety($organizationId)['id'];
+
+        $res = $this->put(
+            "thirdparties/$socId",
+            ['client' => 2],
+        );
+
+        if ($res->getStatusCode() !== 200) {
+            throw new HttpException($res->getStatusCode(), 'Error while updating the society in Dolibarr');
+        }
+    }
 }

+ 37 - 1
src/Service/File/FileManager.php

@@ -13,11 +13,13 @@ use App\Entity\Organization\Organization;
 use App\Entity\Person\Person;
 use App\Enum\Core\FileTypeEnum;
 use App\Enum\Core\FileVisibilityEnum;
+use App\Repository\Core\FileRepository;
 use App\Service\File\Exception\FileNotFoundException;
 use App\Service\File\Factory\ImageFactory;
 use App\Service\File\Storage\FileStorageInterface;
 use App\Service\File\Storage\LocalStorage;
 use App\Service\ServiceIterator\StorageIterator;
+use Doctrine\ORM\EntityManagerInterface;
 
 /**
  * Le gestionnaire de fichiers permet d'effectuer de nombreuses opérations sur les fichiers stockés dans les différents
@@ -29,7 +31,9 @@ class FileManager
         protected readonly IriConverterInterface $iriConverter,
         protected readonly StorageIterator $storageIterator,
         protected readonly ImageFactory $imageFactory,
-        protected readonly LocalStorage $localStorage
+        protected readonly LocalStorage $localStorage,
+        protected readonly EntityManagerInterface $entityManager,
+        protected readonly FileRepository $fileRepository,
     ) {
     }
 
@@ -137,4 +141,36 @@ class FileManager
             ['fileId' => $file->getId()]
         );
     }
+
+    /**
+     * Permanently delete the organization's files from each storage, and remove any reference
+     * in the DB
+     *
+     * @param int $organizationId
+     * @return void
+     */
+    public function deleteOrganizationFiles(int $organizationId): void
+    {
+        foreach ($this->storageIterator->getStorages() as $storageService) {
+            $storageService->deleteOrganizationFiles($organizationId);
+        }
+
+        $this->fileRepository->deleteByOrganization($organizationId);
+    }
+
+    /**
+     * Permanently delete the person's files from each storage, and remove any reference
+     * * in the DB
+ *
+     * @param int $personId
+     * @return void
+     */
+    public function deletePersonFiles(int $personId): void
+    {
+        foreach ($this->storageIterator->getStorages() as $storageService) {
+            $storageService->deletePersonFiles($personId);
+        }
+
+        $this->fileRepository->deleteByPerson($personId);
+    }
 }

+ 24 - 0
src/Service/File/Storage/ApiLegacyStorage.php

@@ -52,4 +52,28 @@ class ApiLegacyStorage implements FileStorageInterface
     {
         return $file->getHost() === FileHostEnum::API1;
     }
+
+    /**
+     * Permanently delete the entire file storage of the given Organization
+     *
+     * @param int $organizationId
+     * @return void
+     */
+    public function deleteOrganizationFiles(int $organizationId): void
+    {
+        $url = sprintf('/_internal/request/organization-files/delete/%s', $organizationId);
+        $this->apiLegacyRequestService->get($url);
+    }
+
+    /**
+     * Permanently delete the entire file storage of the given Person
+     *
+     * @param int $personId
+     * @return void
+     */
+    public function deletePersonFiles(int $personId): void
+    {
+        $url = sprintf('/_internal/request/person-files/delete/%s', $personId);
+        $this->apiLegacyRequestService->get($url);
+    }
 }

+ 4 - 0
src/Service/File/Storage/FileStorageInterface.php

@@ -15,4 +15,8 @@ interface FileStorageInterface
     public function getImageUrl(File $file, string $size, bool $relativePath): string;
 
     public function support(File $file): bool;
+
+    public function deleteOrganizationFiles(int $organizationId): void;
+
+    public function deletePersonFiles(int $personId): void;
 }

+ 49 - 7
src/Service/File/Storage/LocalStorage.php

@@ -47,14 +47,15 @@ class LocalStorage implements FileStorageInterface
     protected FilesystemInterface $filesystem;
 
     public function __construct(
-        protected readonly FilesystemMap $filesystemMap,
+        protected readonly FilesystemMap          $filesystemMap,
         protected readonly EntityManagerInterface $entityManager,
-        protected readonly AccessRepository $accessRepository,
-        protected readonly DataManager $dataManager,
-        protected readonly CacheManager $cacheManager,
-        protected readonly ImageFactory $imageFactory,
-        protected readonly FileUtils $fileUtils,
-        protected readonly UrlBuilder $urlBuilder
+        protected readonly AccessRepository       $accessRepository,
+        protected readonly DataManager            $dataManager,
+        protected readonly CacheManager           $cacheManager,
+        protected readonly ImageFactory           $imageFactory,
+        protected readonly FileUtils              $fileUtils,
+        protected readonly UrlBuilder             $urlBuilder,
+        protected readonly string                 $fileStorageDir
     ) {
         $this->filesystem = $filesystemMap->get(static::FS_KEY);
     }
@@ -294,6 +295,47 @@ class LocalStorage implements FileStorageInterface
         }
     }
 
+    /**
+     * Permanently delete the entire file storage of the given Organization
+     *
+     * @param int $organizationId
+     * @return void
+     */
+    public function deleteOrganizationFiles(int $organizationId): void
+    {
+        $this->rrmDir('organization/' . $organizationId);
+        $this->rrmDir('temp/organization/' . $organizationId);
+    }
+
+    /**
+     * Permanently delete the entire file storage of the given Person
+     *
+     * @param int $personId
+     * @return void
+     */
+    public function deletePersonFiles(int $personId): void
+    {
+        $this->rrmDir('person/' . $personId);
+        $this->rrmDir('temp/person/' . $personId);
+    }
+
+    /**
+     * Supprime récursivement un répertoire
+     *
+     * (Au moment du développement, Gaufrette ne permet pas la suppression de répertoire, on laissera
+     * le soin à un cron de supprimer les répertoires vides du storage)
+     *
+     * @param string $dirKey
+     * @return void
+     */
+    protected function rrmDir(string $dirKey): void {
+        if (!$this->filesystem->isDirectory($dirKey)) {
+            throw new \RuntimeException('Directory `'.$dirKey.'` does not exist');
+        }
+        $dir = Path::join($this->fileStorageDir, $dirKey);
+        Path::rmtree($dir);
+    }
+
     /**
      * If an organization owns the file, the prefix will be '(_temp_/)organization/{id}(/{type})'.
      * If a person owns it, the prefix will be '(_temp_/)person/{id}(/{type})'

+ 7 - 7
src/Service/OnChange/Organization/OnParametersChange.php

@@ -8,9 +8,9 @@ use App\Entity\Booking\Course;
 use App\Entity\Education\EducationNotationConfig;
 use App\Entity\Organization\Parameters;
 use App\Enum\Education\AdvancedEducationNotationTypeEnum;
-use App\Message\Command\Typo3\Typo3DeleteCommand;
-use App\Message\Command\Typo3\Typo3UndeleteCommand;
-use App\Message\Command\Typo3\Typo3UpdateCommand;
+use App\Message\Message\Typo3\Typo3Delete;
+use App\Message\Message\Typo3\Typo3Undelete;
+use App\Message\Message\Typo3\Typo3Update;
 use App\Repository\Booking\CourseRepository;
 use App\Service\Network\Utils as NetworkUtils;
 use App\Service\OnChange\OnChangeContext;
@@ -86,7 +86,7 @@ class OnParametersChange extends OnChangeDefault
             && $context->previousData()->getCustomDomain() !== $parameters->getCustomDomain()
         ) {
             $this->messageBus->dispatch(
-                new Typo3UpdateCommand($parameters->getOrganization()->getId())
+                new Typo3Update($parameters->getOrganization()->getId())
             );
         }
 
@@ -97,14 +97,14 @@ class OnParametersChange extends OnChangeDefault
         ) {
             if ($parameters->getDesactivateOpentalentSiteWeb()) {
                 $this->messageBus->dispatch(
-                    new Typo3DeleteCommand($parameters->getOrganization()->getId())
+                    new Typo3Delete($parameters->getOrganization()->getId())
                 );
             } else {
                 $this->messageBus->dispatch(
-                    new Typo3UndeleteCommand($parameters->getOrganization()->getId())
+                    new Typo3Undelete($parameters->getOrganization()->getId())
                 );
                 $this->messageBus->dispatch(
-                    new Typo3UpdateCommand($parameters->getOrganization()->getId())
+                    new Typo3Update($parameters->getOrganization()->getId())
                 );
             }
         }

+ 154 - 0
src/Service/Organization/OrganizationFactory.php

@@ -3,6 +3,7 @@
 namespace App\Service\Organization;
 
 use App\ApiResources\Organization\OrganizationCreationRequest;
+use App\ApiResources\Organization\OrganizationDeletionRequest;
 use App\ApiResources\Organization\OrganizationMemberCreationRequest;
 use App\Entity\Access\Access;
 use App\Entity\Access\OrganizationFunction;
@@ -31,12 +32,14 @@ use App\Repository\Organization\OrganizationRepository;
 use App\Repository\Person\PersonRepository;
 use App\Service\ApiLegacy\ApiLegacyRequestService;
 use App\Service\Dolibarr\DolibarrApiService;
+use App\Service\File\FileManager;
 use App\Service\Organization\Utils as OrganizationUtils;
 use App\Service\Typo3\BindFileService;
 use App\Service\Typo3\SubdomainService;
 use App\Service\Typo3\Typo3Service;
 use App\Service\Utils\DatesUtils;
 use App\Service\Utils\UrlBuilder;
+use App\Service\Utils\SecurityUtils;
 use Doctrine\ORM\EntityManagerInterface;
 use Elastica\Param;
 use libphonenumber\NumberParseException;
@@ -145,6 +148,7 @@ class OrganizationFactory
             $this->logger->debug($e);
             $withError = true;
         }
+
         // Création du site typo3 (on est obligé d'attendre que l'organisation soit persistée en base)
         if ($organizationCreationRequest->getCreateWebsite()) {
             try {
@@ -776,4 +780,154 @@ class OrganizationFactory
 
         return preg_replace('/[^a-z0-9]+/u', '+', $value);
     }
+
+    /**
+     * /!\ Danger zone /!\.
+     *
+     * Supprime définitivement une organisation, ses données, ses fichiers, son site internet, et son profil Dolibarr.
+     *
+     * Pour éviter une suppression accidentelle, cette méthode ne doit pouvoir être exécutée que si la requête a été
+     * envoyée depuis le localhost.
+     *
+     * @throws \Exception
+     */
+    public function delete(OrganizationDeletionRequest $organizationDeletionRequest): OrganizationDeletionRequest
+    {
+        SecurityUtils::preventIfNotLocalhost();
+
+        $organization = $this->organizationRepository->find($organizationDeletionRequest->getOrganizationId());
+        if (!$organization) {
+            throw new \RuntimeException("No organization was found for id : " . $organizationDeletionRequest->getOrganizationId());
+        }
+
+        $this->logger->info(
+            "Start the deletion of organization '".$organization->getName()."' [".$organization->getId().']'
+        );
+
+        $this->entityManager->beginTransaction();
+
+        $withError = false;
+
+        try {
+            $orphanPersons = $this->getOrphansToBePersons($organization);
+
+            // On est obligé de supprimer manuellement les paramètres, car c'est l'entité Parameters qui est
+            // propriétaire de la relation Organization ↔ Parameters.
+            $this->entityManager->remove($organization->getParameters());
+
+            // Toutes les autres entités liées seront supprimées en cascade
+            $this->entityManager->remove($organization);
+
+            // Supprime les personnes qui n'avaient pas d'autre Access attaché
+            $deletedPersonIds = [];
+            foreach ($orphanPersons as $person) {
+                $deletedPersonIds[] = $person->getId();
+                $this->entityManager->remove($person);
+            }
+
+            $this->entityManager->flush();
+            $this->entityManager->commit();
+        } catch (\Exception $e) {
+            $this->logger->critical("An error happened, operation cancelled\n".$e);
+            $this->entityManager->rollback();
+            throw $e;
+        }
+
+        try {
+            $this->deleteTypo3Website($organizationDeletionRequest->getOrganizationId());
+        } catch (\Exception $e) {
+            $this->logger->critical('An error happened while deleting the Typo3 website, please proceed manually.');
+            $this->logger->debug($e);
+            $withError = true;
+        }
+
+        try {
+            $this->switchDolibarrSocietyToProspect($organization);
+        } catch (\Exception $e) {
+            $this->logger->critical('An error happened while updating the Dolibarr society, please proceed manually.');
+            $this->logger->debug($e);
+            $withError = true;
+        }
+
+        try {
+            $this->fileManager->deleteOrganizationFiles($organizationDeletionRequest->getOrganizationId());
+        } catch (\Exception $e) {
+            $this->logger->critical("An error happened while deleting the organization's files, please proceed manually.");
+            $this->logger->debug($e);
+            $withError = true;
+        }
+
+        foreach ($deletedPersonIds as $personId) {
+            try {
+                $this->fileManager->deletePersonFiles($personId);
+            } catch (\Exception $e) {
+                $this->logger->critical("An error happened while deleting the person's files, please proceed manually (id=" . $person->getId() . ").");
+                $this->logger->debug($e);
+                $withError = true;
+            }
+        }
+
+        if ($withError) {
+            $organizationDeletionRequest->setStatus(OrganizationDeletionRequest::STATUS_OK_WITH_ERRORS);
+            $this->logger->warning('-- Operation ended with errors, check the logs for more information --');
+        } else {
+            $organizationDeletionRequest->setStatus(OrganizationDeletionRequest::STATUS_OK);
+        }
+
+        return $organizationDeletionRequest;
+    }
+
+    /**
+     * Supprime tous les Access d'une organisation, ainsi que la Person
+     * rattachée (si celle-ci n'est pas liée à d'autres Access).
+     *
+     * @param Organization $organization
+     * @return array<Person>
+     */
+    protected function getOrphansToBePersons(Organization $organization): array
+    {
+        $orphans = [];
+
+        foreach ($organization->getAccesses() as $access) {
+            $person = $access->getPerson();
+            if ($person->getAccesses()->count() === 1) {
+                $orphans[] = $person;
+            }
+        }
+        return $orphans;
+    }
+
+    // TODO: à revoir, c'est du many to many
+    //    protected function removeTypeOfPractices(Organization $organization): void {
+    //        foreach ($organization->getTypeOfPractices() as $typeOfPractice) {
+    //            $organization->removeTypeOfPractice($typeOfPractice);
+    //        }
+    //    }
+
+    // TODO: à revoir, c'est du many to many
+    //    protected function deleteContactPoints(Organization $organization): void
+    //    {
+    //        foreach ($organization->getContactPoints() as $contactPoint) {
+    //            $this->entityManager->remove($contactPoint);
+    //        }
+    //    }
+
+    // TODO: à revoir, c'est du many to many
+    //    protected function deleteBankAccounts(Organization $organization): void {
+    //        foreach ($organization->getBankAccounts() as $bankAccount) {
+    //            $this->entityManager->remove($bankAccount);
+    //        }
+    //    }
+
+
+    protected function deleteTypo3Website(int $organizationId): void
+    {
+        // TODO: implement
+        //        $this->typo3Service->deleteSite($organization->getId());
+    }
+
+    protected function switchDolibarrSocietyToProspect(Organization $organization): void
+    {
+        $this->dolibarrApiService->switchSocietyToProspect($organization->getId());
+    }
 }

+ 5 - 0
src/Service/ServiceIterator/StorageIterator.php

@@ -22,6 +22,11 @@ class StorageIterator
     ) {
     }
 
+    public function getStorages(): iterable
+    {
+        return $this->storageServices;
+    }
+
     /**
      * Itère sur les services de storage disponibles et
      * retourne le premier qui supporte ce type de requête.

+ 3 - 3
src/Service/Typo3/SubdomainService.php

@@ -4,8 +4,8 @@ namespace App\Service\Typo3;
 
 use App\Entity\Organization\Organization;
 use App\Entity\Organization\Subdomain;
-use App\Message\Command\MailerCommand;
-use App\Message\Command\Typo3\Typo3UpdateCommand;
+use App\Message\Message\MailerCommand;
+use App\Message\Message\Typo3\Typo3Update;
 use App\Repository\Access\AccessRepository;
 use App\Repository\Organization\SubdomainRepository;
 use App\Service\Mailer\Model\SubdomainChangeModel;
@@ -215,7 +215,7 @@ class SubdomainService
     protected function updateTypo3Website(Organization $organization): void
     {
         $this->messageBus->dispatch(
-            new Typo3UpdateCommand($organization->getId())
+            new Typo3Update($organization->getId())
         );
     }
 

+ 1 - 1
src/Service/Utils/Path.php

@@ -96,7 +96,7 @@ class Path
      *
      * @return bool returns true if the directory was successfully removed, false otherwise
      */
-    protected static function rmtree(string $path): bool
+    public static function rmtree(string $path): bool
     {
         if (!file_exists($path)) {
             return true;

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

@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\Utils;
+
+class SecurityUtils
+{
+    /**
+     * Lève une exception si la méthode a été appelée dans le cadre d'un appel API originaire d'un hôte
+     * différent de localhost.
+     */
+    public static function preventIfNotLocalhost(): void
+    {
+        if (
+            $_SERVER
+            && $_SERVER['APP_ENV'] !== 'docker'
+            && $_SERVER['SERVER_ADDR'] !== $_SERVER['REMOTE_ADDR']
+        ) {
+            throw new \RuntimeException('This operation is restricted to localhost');
+        }
+    }
+}

+ 1 - 1
src/State/Processor/Export/LicenceCmf/ExportRequestProcessor.php

@@ -10,7 +10,7 @@ use ApiPlatform\State\ProcessorInterface;
 use App\ApiResources\Export\ExportRequest;
 use App\Entity\Access\Access;
 use App\Entity\Core\File;
-use App\Message\Command\Export;
+use App\Message\Message\Export;
 use App\Service\ServiceIterator\ExporterIterator;
 use Symfony\Bundle\SecurityBundle\Security;
 use Symfony\Component\HttpFoundation\Response;

+ 12 - 7
src/State/Processor/Organization/OrganizationCreationRequestProcessor.php

@@ -4,12 +4,12 @@ declare(strict_types=1);
 
 namespace App\State\Processor\Organization;
 
+use App\Entity\Access\Access;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\Metadata\Post;
 use ApiPlatform\State\ProcessorInterface;
 use App\ApiResources\Organization\OrganizationCreationRequest;
-use App\Entity\Access\Access;
-use App\Message\Command\OrganizationCreationCommand;
+use App\Message\Message\OrganizationCreation;
 use App\Service\Organization\OrganizationFactory;
 use App\Service\Utils\DatesUtils;
 use Symfony\Bundle\SecurityBundle\Security;
@@ -26,14 +26,19 @@ class OrganizationCreationRequestProcessor implements ProcessorInterface
     }
 
     /**
-     * @param OrganizationCreationRequest $organizationCreationRequest
-     * @param mixed[]                     $uriVariables
-     * @param mixed[]                     $context
+     * @param OrganizationCreationRequest $data
+     * @param mixed[]       $uriVariables
+     * @param mixed[]       $context
      *
      * @throws \Exception
      */
-    public function process(mixed $organizationCreationRequest, Operation $operation, array $uriVariables = [], array $context = []): OrganizationCreationRequest
+    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): OrganizationCreationRequest
     {
+        /**
+         * @var OrganizationCreationRequest $organizationCreationRequest
+         */
+        $organizationCreationRequest = $data;
+
         if (!$operation instanceof Post) {
             throw new \RuntimeException('not supported', Response::HTTP_METHOD_NOT_ALLOWED);
         }
@@ -47,7 +52,7 @@ class OrganizationCreationRequestProcessor implements ProcessorInterface
         if ($organizationCreationRequest->isAsync()) {
             // Send the export request to Messenger (@see App\Message\Handler\OrganizationCreationHandler)
             $this->messageBus->dispatch(
-                new OrganizationCreationCommand($organizationCreationRequest)
+                new OrganizationCreation($organizationCreationRequest)
             );
         } else {
             // For testing purposes only

+ 55 - 0
src/State/Processor/Organization/OrganizationDeletionRequestProcessor.php

@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\State\Processor\Organization;
+
+use ApiPlatform\Metadata\Delete;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\State\ProcessorInterface;
+use App\ApiResources\Organization\OrganizationCreationRequest;
+use App\ApiResources\Organization\OrganizationDeletionRequest;
+use App\Message\Message\OrganizationCreation;
+use App\Message\Message\OrganizationDeletion;
+use App\Service\Organization\OrganizationFactory;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Messenger\MessageBusInterface;
+
+class OrganizationDeletionRequestProcessor implements ProcessorInterface
+{
+    public function __construct(
+        private readonly MessageBusInterface $messageBus,
+        private readonly OrganizationFactory $organizationFactory,
+    ) {}
+
+    /**
+     * @param OrganizationDeletionRequest $data
+     * @param mixed[]       $uriVariables
+     * @param mixed[]       $context
+     *
+     * @throws \Exception
+     */
+    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): OrganizationDeletionRequest
+    {
+        /**
+         * @var OrganizationDeletionRequest $organizationDeletionRequest
+         */
+        $organizationDeletionRequest = $data;
+
+        if (!$operation instanceof Post) {
+            throw new \RuntimeException('not supported', Response::HTTP_METHOD_NOT_ALLOWED);
+        }
+
+        if ($organizationDeletionRequest->isAsync()) {
+            // Send the export request to Messenger (@see App\Message\Handler\OrganizationCreationHandler)
+            $this->messageBus->dispatch(
+                new OrganizationDeletion($organizationDeletionRequest)
+            );
+        } else {
+            // For testing purposes only
+            $this->organizationFactory->delete($organizationDeletionRequest);
+        }
+        return $organizationDeletionRequest;
+    }
+}

+ 55 - 4
tests/Unit/Service/Dolibarr/DolibarrApiServiceTest.php

@@ -461,7 +461,7 @@ class DolibarrApiServiceTest extends TestCase
             'client' => 2,
             'code_client' => -1,
             'import_key' => 'crm',
-            'array_options' => ['options_2iopen_organization_id' => 123]
+            'array_options' => ['options_2iopen_organization_id' => 123],
         ];
 
         $response = $this->getMockBuilder(ResponseInterface::class)->getMock();
@@ -470,7 +470,7 @@ class DolibarrApiServiceTest extends TestCase
         $dolibarrApiService
             ->expects(self::once())
             ->method('post')
-            ->with("/thirdparties", $expectedPostBody)
+            ->with('/thirdparties', $expectedPostBody)
             ->willReturn($response);
 
         $result = $dolibarrApiService->createSociety($organization);
@@ -497,7 +497,7 @@ class DolibarrApiServiceTest extends TestCase
             'client' => 1,
             'code_client' => -1,
             'import_key' => 'crm',
-            'array_options' => ['options_2iopen_organization_id' => 123]
+            'array_options' => ['options_2iopen_organization_id' => 123],
         ];
 
         $response = $this->getMockBuilder(ResponseInterface::class)->getMock();
@@ -506,11 +506,62 @@ class DolibarrApiServiceTest extends TestCase
         $dolibarrApiService
             ->expects(self::once())
             ->method('post')
-            ->with("/thirdparties", $expectedPostBody)
+            ->with('/thirdparties', $expectedPostBody)
             ->willReturn($response);
 
         $result = $dolibarrApiService->createSociety($organization, true);
 
         $this->assertEquals(456, $result);
     }
+
+    public function testSwitchSocietyToProspect(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['switchSocietyToProspect'])
+            ->getMock();
+
+        $response = $this->getMockBuilder(ResponseInterface::class)->getMock();
+        $response->method('getStatusCode')->willReturn(200);
+
+        $dolibarrApiService
+            ->method('getSociety')
+            ->with(123)
+            ->willReturn(['id' => 456]);
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('put')
+            ->with('thirdparties/456', ['client' => 2])
+            ->willReturn($response);
+
+        $dolibarrApiService->switchSocietyToProspect(123);
+    }
+
+    public function testSwitchSocietyToProspectWithError(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['switchSocietyToProspect'])
+            ->getMock();
+
+        $response = $this->getMockBuilder(ResponseInterface::class)->getMock();
+        $response->method('getStatusCode')->willReturn(500);
+
+        $dolibarrApiService
+            ->method('getSociety')
+            ->with(123)
+            ->willReturn(['id' => 456]);
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('put')
+            ->with('thirdparties/456', ['client' => 2])
+            ->willReturn($response);
+
+        $this->expectException(HttpException::class);
+        $this->expectExceptionMessage('Error while updating the society in Dolibarr');
+
+        $dolibarrApiService->switchSocietyToProspect(123);
+    }
 }

+ 11 - 11
tests/Unit/Service/OnChange/Organization/OnParametersChangeTest.php

@@ -11,9 +11,9 @@ use App\Entity\Education\EducationNotationConfig;
 use App\Entity\Organization\Organization;
 use App\Entity\Organization\Parameters;
 use App\Enum\Education\AdvancedEducationNotationTypeEnum;
-use App\Message\Command\Typo3\Typo3DeleteCommand;
-use App\Message\Command\Typo3\Typo3UndeleteCommand;
-use App\Message\Command\Typo3\Typo3UpdateCommand;
+use App\Message\Message\Typo3\Typo3Delete;
+use App\Message\Message\Typo3\Typo3Undelete;
+use App\Message\Message\Typo3\Typo3Update;
 use App\Repository\Booking\CourseRepository;
 use App\Service\Network\Utils as NetworkUtils;
 use App\Service\OnChange\OnChangeContext;
@@ -206,8 +206,8 @@ class OnParametersChangeTest extends TestCase
         $this->messageBus
             ->expects(self::once())
             ->method('dispatch')
-            ->with(self::isInstanceOf(Typo3UpdateCommand::class))
-            ->willReturn(new Envelope(new Typo3UpdateCommand(1)));
+            ->with(self::isInstanceOf(Typo3Update::class))
+            ->willReturn(new Envelope(new Typo3Update(1)));
 
         $previousParameters = $this->getMockBuilder(Parameters::class)->getMock();
         $previousParameters->method('getId')->willReturn(1);
@@ -245,8 +245,8 @@ class OnParametersChangeTest extends TestCase
         $this->messageBus
             ->expects(self::once())
             ->method('dispatch')
-            ->with(self::isInstanceOf(Typo3DeleteCommand::class))
-            ->willReturn(new Envelope(new Typo3DeleteCommand(1)));
+            ->with(self::isInstanceOf(Typo3Delete::class))
+            ->willReturn(new Envelope(new Typo3Delete(1)));
 
         $previousParameters = $this->getMockBuilder(Parameters::class)->getMock();
         $previousParameters->method('getId')->willReturn(1);
@@ -285,11 +285,11 @@ class OnParametersChangeTest extends TestCase
             ->expects(self::exactly(2))
             ->method('dispatch')
             ->willReturnCallback(function ($message) {
-                if ($message instanceof Typo3UndeleteCommand) {
-                    return new Envelope(new Typo3UndeleteCommand(1));
+                if ($message instanceof Typo3Undelete) {
+                    return new Envelope(new Typo3Undelete(1));
                 }
-                if ($message instanceof Typo3UpdateCommand) {
-                    return new Envelope(new Typo3UpdateCommand(1));
+                if ($message instanceof Typo3Update) {
+                    return new Envelope(new Typo3Update(1));
                 }
                 throw new \AssertionError('unexpected message : '.$message::class);
             });

+ 238 - 0
tests/Unit/Service/Organization/OrganizationFactoryTest.php

@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace App\Tests\Unit\Service\Organization;
 
 use App\ApiResources\Organization\OrganizationCreationRequest;
+use App\ApiResources\Organization\OrganizationDeletionRequest;
 use App\ApiResources\Organization\OrganizationMemberCreationRequest;
 use App\Entity\Access\Access;
 use App\Entity\Access\FunctionType;
@@ -43,6 +44,7 @@ use App\Service\Typo3\BindFileService;
 use App\Service\Typo3\SubdomainService;
 use App\Service\Typo3\Typo3Service;
 use App\Service\Utils\DatesUtils;
+use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\ORM\EntityManagerInterface;
 use libphonenumber\PhoneNumber;
 use libphonenumber\PhoneNumberUtil;
@@ -149,6 +151,36 @@ class TestableOrganizationFactory extends OrganizationFactory
     {
         return parent::normalizeIdentificationField($value);
     }
+
+    public function deleteOrganizationAccesses(Organization $organization): void
+    {
+        parent::deleteOrganizationAccesses($organization);
+    }
+
+    public function deleteTypo3Website(Organization $organization): void
+    {
+        parent::deleteTypo3Website($organization);
+    }
+
+    public function switchDolibarrSocietyToProspect(Organization $organization): void
+    {
+        parent::switchDolibarrSocietyToProspect($organization);
+    }
+
+    public function deleteOrganizationFiles(Organization $organization): void
+    {
+        parent::deleteOrganizationFiles($organization);
+    }
+
+    public function deleteDirectoriesV1(Organization $organization): void
+    {
+        parent::deleteDirectoriesV1($organization);
+    }
+
+    public function deleteDirectories59(Organization $organization): void
+    {
+        parent::deleteDirectories59($organization);
+    }
 }
 
 class OrganizationFactoryTest extends TestCase
@@ -1903,4 +1935,210 @@ class OrganizationFactoryTest extends TestCase
             $organizationFactory->normalizeIdentificationField("C'est une phrase normalisée.")
         );
     }
+    public function testDelete(): void
+    {
+        $organizationFactory = $this->getOrganizationFactoryMockFor('delete');
+
+        $organizationDeletionRequest = $this->getMockBuilder(OrganizationDeletionRequest::class)->getMock();
+
+        $organizationDeletionRequest->method('getOrganizationId')->willReturn(123);
+
+        $parameters = $this->getMockBuilder(Parameters::class)->getMock();
+        $organization = $this->getMockBuilder(Organization::class)->getMock();
+        $organization->method('getId')->willReturn(123);
+        $organization->method('getParameters')->willReturn($parameters);
+
+        $this->organizationRepository
+            ->expects(self::once())
+            ->method('find')
+            ->with(123)
+            ->willReturn($organization);
+
+        $this->entityManager->expects(self::once())->method('beginTransaction');
+        $this->entityManager->expects(self::once())->method('flush');
+        $this->entityManager->expects(self::once())->method('commit');
+        $this->entityManager->expects(self::never())->method('rollback');
+
+        $organizationFactory->expects(self::once())->method('deleteOrganizationAccesses')->with($organization);
+
+        $this->entityManager->expects(self::exactly(2))->method('remove')->withConsecutive(
+            [$parameters],
+            [$organization]
+        );
+
+        $organizationFactory->expects(self::once())->method('deleteTypo3Website')->with($organization);
+        $organizationFactory->expects(self::once())->method('switchDolibarrSocietyToProspect')->with($organization);
+        $organizationFactory->expects(self::once())->method('deleteOrganizationFiles')->with($organization);
+        $organizationFactory->expects(self::once())->method('deleteDirectoriesV1')->with($organization);
+        $organizationFactory->expects(self::once())->method('deleteDirectories59')->with($organization);
+
+        $organizationDeletionRequest
+            ->expects(self::once())
+            ->method('setStatus')
+            ->with(OrganizationDeletionRequest::STATUS_OK);
+
+        $result = $organizationFactory->delete($organizationDeletionRequest);
+
+        $this->assertEquals(
+            $organizationDeletionRequest,
+            $result
+        );
+    }
+
+    public function testDeleteWithRollback(): void
+    {
+        $organizationFactory = $this->getOrganizationFactoryMockFor('delete');
+
+        $organizationDeletionRequest = $this->getMockBuilder(OrganizationDeletionRequest::class)->getMock();
+
+        $organizationDeletionRequest->method('getOrganizationId')->willReturn(123);
+
+        $parameters = $this->getMockBuilder(Parameters::class)->getMock();
+        $organization = $this->getMockBuilder(Organization::class)->getMock();
+        $organization->method('getParameters')->willReturn($parameters);
+
+        $this->organizationRepository
+            ->expects(self::once())
+            ->method('find')
+            ->with(123)
+            ->willReturn($organization);
+
+        $this->entityManager->expects(self::once())->method('beginTransaction');
+        $this->entityManager->expects(self::never())->method('flush');
+        $this->entityManager->expects(self::never())->method('commit');
+        $this->entityManager->expects(self::once())->method('rollback');
+
+        $organizationFactory->expects(self::once())->method('deleteOrganizationAccesses')->with($organization);
+
+        $this->entityManager->method('remove')->willThrowException(new \Exception('some error'));
+
+        $organizationFactory->expects(self::never())->method('deleteTypo3Website');
+        $organizationFactory->expects(self::never())->method('switchDolibarrSocietyToProspect');
+        $organizationFactory->expects(self::never())->method('deleteOrganizationFiles');
+        $organizationFactory->expects(self::never())->method('deleteDirectoriesV1');
+        $organizationFactory->expects(self::never())->method('deleteDirectories59');
+
+        $organizationDeletionRequest
+            ->expects(self::never())
+            ->method('setStatus');
+
+        $this->logger
+            ->expects(self::once())
+            ->method('critical')
+            ->with($this->callback(function ($arg) {
+                return is_string($arg) && str_contains($arg, 'An error happened, operation cancelled') && str_contains($arg, 'some error');
+            }));
+
+        $this->expectException(\Exception::class);
+
+        $organizationFactory->delete($organizationDeletionRequest);
+    }
+
+    public function testDeleteWithNonBlockingErrors(): void
+    {
+        $organizationFactory = $this->getOrganizationFactoryMockFor('delete');
+
+        $organizationDeletionRequest = $this->getMockBuilder(OrganizationDeletionRequest::class)->getMock();
+
+        $organizationDeletionRequest->method('getOrganizationId')->willReturn(123);
+
+        $parameters = $this->getMockBuilder(Parameters::class)->getMock();
+        $organization = $this->getMockBuilder(Organization::class)->getMock();
+        $organization->method('getId')->willReturn(123);
+        $organization->method('getParameters')->willReturn($parameters);
+
+        $this->organizationRepository
+            ->expects(self::once())
+            ->method('find')
+            ->with(123)
+            ->willReturn($organization);
+
+        $this->entityManager->expects(self::once())->method('beginTransaction');
+        $this->entityManager->expects(self::once())->method('flush');
+        $this->entityManager->expects(self::once())->method('commit');
+        $this->entityManager->expects(self::never())->method('rollback');
+
+        $organizationFactory->expects(self::once())->method('deleteOrganizationAccesses')->with($organization);
+
+        $this->entityManager->expects(self::exactly(2))->method('remove')->withConsecutive(
+            [$parameters],
+            [$organization]
+        );
+
+        $organizationFactory->expects(self::once())->method('deleteTypo3Website')->willThrowException(new \Exception('some error'));
+        $organizationFactory->expects(self::once())->method('switchDolibarrSocietyToProspect')->willThrowException(new \Exception('some error'));
+        $organizationFactory->expects(self::once())->method('deleteOrganizationFiles')->willThrowException(new \Exception('some error'));
+        $organizationFactory->expects(self::once())->method('deleteDirectoriesV1')->willThrowException(new \Exception('some error'));
+        $organizationFactory->expects(self::once())->method('deleteDirectories59')->willThrowException(new \Exception('some error'));
+
+        $organizationDeletionRequest
+            ->expects(self::once())
+            ->method('setStatus')
+            ->with(OrganizationDeletionRequest::STATUS_OK_WITH_ERRORS);
+
+        $this->logger
+            ->expects(self::exactly(5))
+            ->method('critical')
+        ->withConsecutive(
+            ['An error happened while deleting the Typo3 website, please proceed manually.'],
+            ['An error happened while updating the Dolibarr society, please proceed manually.'],
+            ['An error happened while deleting the local directories, please proceed manually.'],
+            ['An error happened while deleting the V1 directories, please proceed manually.'],
+            ['An error happened while deleting the 5.9 directories, please proceed manually.'],
+        );
+
+        $result = $organizationFactory->delete($organizationDeletionRequest);
+
+        $this->assertEquals(
+            $organizationDeletionRequest,
+            $result
+        );
+    }
+
+    public function testDeleteOrganizationAccesses(): void
+    {
+        $organizationFactory = $this->getOrganizationFactoryMockFor('deleteOrganizationAccesses');
+
+        $organization = $this->getMockBuilder(Organization::class)->getMock();
+
+        $access1 = $this->getMockBuilder(Access::class)->getMock();
+        $access2 = $this->getMockBuilder(Access::class)->getMock();
+        $access_other = $this->getMockBuilder(Access::class)->getMock();
+
+        $person1 = $this->getMockBuilder(Person::class)->getMock();
+        $person1->method('getAccesses')->willReturn(new ArrayCollection([$access1]));
+        $access1->method('getPerson')->willReturn($person1);
+
+        $person2 = $this->getMockBuilder(Person::class)->getMock();
+        $person2->method('getAccesses')->willReturn(new ArrayCollection([$access2, $access_other]));
+        $access2->method('getPerson')->willReturn($person2);
+
+        $organization->method('getAccesses')->willReturn(new ArrayCollection([$access1, $access2]));
+
+        $this->entityManager
+            ->expects(self::exactly(3))
+            ->method('remove')
+            ->withConsecutive(
+                [$person1],
+                [$access1],
+                [$access2],
+            );
+
+        $organizationFactory->deleteOrganizationAccesses($organization);
+    }
+
+    public function testSwitchDolibarrSocietyToProspect(): void
+    {
+        $organizationFactory = $this->getOrganizationFactoryMockFor('switchDolibarrSocietyToProspect');
+
+        $organization = $this->getMockBuilder(Organization::class)->getMock();
+        $organization->method('getId')->willReturn(123);
+
+        $this->dolibarrApiService
+            ->expects(self::once())
+            ->method('switchSocietyToProspect')
+            ->with(123);
+
+        $organizationFactory->switchDolibarrSocietyToProspect($organization);
+    }
 }

+ 4 - 4
tests/Unit/Service/Typo3/SubdomainServiceTest.php

@@ -7,8 +7,8 @@ use App\Entity\Organization\Organization;
 use App\Entity\Organization\Parameters;
 use App\Entity\Organization\Subdomain;
 use App\Entity\Person\Person;
-use App\Message\Command\MailerCommand;
-use App\Message\Command\Typo3\Typo3UpdateCommand;
+use App\Message\Message\MailerCommand;
+use App\Message\Message\Typo3\Typo3Update;
 use App\Repository\Access\AccessRepository;
 use App\Repository\Organization\SubdomainRepository;
 use App\Service\Mailer\Model\SubdomainChangeModel;
@@ -471,8 +471,8 @@ class SubdomainServiceTest extends TestCase
         $this->messageBus
             ->expects(self::once())
             ->method('dispatch')
-            ->with(self::isInstanceOf(Typo3UpdateCommand::class))
-            ->willReturn(new Envelope(new Typo3UpdateCommand(1)));
+            ->with(self::isInstanceOf(Typo3Update::class))
+            ->willReturn(new Envelope(new Typo3Update(1)));
 
         $subdomainService->updateTypo3Website($organization);
     }