OrganizationFactory.php 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Service\Organization;
  4. use App\ApiResources\Organization\OrganizationCreationRequest;
  5. use App\ApiResources\Organization\OrganizationDeletionRequest;
  6. use App\ApiResources\Organization\OrganizationMemberCreationRequest;
  7. use App\Entity\Access\Access;
  8. use App\Entity\Access\OrganizationFunction;
  9. use App\Entity\Core\AddressPostal;
  10. use App\Entity\Core\ContactPoint;
  11. use App\Entity\Education\Cycle;
  12. use App\Entity\Network\NetworkOrganization;
  13. use App\Entity\Organization\Organization;
  14. use App\Entity\Organization\OrganizationAddressPostal;
  15. use App\Entity\Organization\Parameters;
  16. use App\Entity\Organization\Settings;
  17. use App\Entity\Organization\Subdomain;
  18. use App\Entity\Person\Person;
  19. use App\Entity\Person\PersonAddressPostal;
  20. use App\Enum\Access\FunctionEnum;
  21. use App\Enum\Core\ContactPointTypeEnum;
  22. use App\Enum\Education\CycleEnum;
  23. use App\Enum\Network\NetworkEnum;
  24. use App\Enum\Organization\AddressPostalOrganizationTypeEnum;
  25. use App\Enum\Organization\SettingsProductEnum;
  26. use App\Enum\Person\AddressPostalPersonTypeEnum;
  27. use App\Repository\Access\FunctionTypeRepository;
  28. use App\Repository\Core\CountryRepository;
  29. use App\Repository\Organization\OrganizationIdentificationRepository;
  30. use App\Repository\Organization\OrganizationRepository;
  31. use App\Repository\Person\PersonRepository;
  32. use App\Service\ApiLegacy\ApiLegacyRequestService;
  33. use App\Service\Dolibarr\DolibarrApiService;
  34. use App\Service\File\FileManager;
  35. use App\Service\Organization\Utils as OrganizationUtils;
  36. use App\Service\Typo3\BindFileService;
  37. use App\Service\Typo3\SubdomainService;
  38. use App\Service\Typo3\Typo3Service;
  39. use App\Service\Utils\DatesUtils;
  40. use App\Service\Utils\SecurityUtils;
  41. use App\Service\Utils\UrlBuilder;
  42. use Doctrine\ORM\EntityManagerInterface;
  43. use libphonenumber\NumberParseException;
  44. use libphonenumber\PhoneNumberUtil;
  45. use Psr\Log\LoggerInterface;
  46. use Symfony\Component\HttpFoundation\Response;
  47. use Symfony\Component\String\ByteString;
  48. use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
  49. use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
  50. use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
  51. use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
  52. use Symfony\Contracts\Service\Attribute\Required;
  53. class OrganizationFactory
  54. {
  55. private LoggerInterface $logger;
  56. protected PhoneNumberUtil $phoneNumberUtil;
  57. /**
  58. * Regex pattern for password validation.
  59. * Requires at least 8 characters, including at least one uppercase letter,
  60. * one lowercase letter, one digit, and one special character.
  61. */
  62. private const PASSWORD_PATTERN = '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\da-zA-Z]).{8,}$/';
  63. public function __construct(
  64. private readonly SubdomainService $subdomainService,
  65. private readonly OrganizationRepository $organizationRepository,
  66. private readonly CountryRepository $countryRepository,
  67. private readonly OrganizationUtils $organizationUtils,
  68. private readonly Typo3Service $typo3Service,
  69. private readonly DolibarrApiService $dolibarrApiService,
  70. private readonly EntityManagerInterface $entityManager,
  71. private readonly PersonRepository $personRepository,
  72. private readonly BindFileService $bindFileService,
  73. private readonly OrganizationIdentificationRepository $organizationIdentificationRepository,
  74. private readonly ApiLegacyRequestService $apiLegacyRequestService,
  75. private readonly FunctionTypeRepository $functionTypeRepository,
  76. private readonly FileManager $fileManager,
  77. private readonly \App\Repository\Access\AccessRepository $accessRepository,
  78. ) {
  79. $this->phoneNumberUtil = PhoneNumberUtil::getInstance();
  80. }
  81. #[Required]
  82. /** @see https://symfony.com/doc/current/logging/channels_handlers.html#how-to-autowire-logger-channels */
  83. public function setLoggerInterface(LoggerInterface $adminLogger): void
  84. {
  85. $this->logger = $adminLogger;
  86. }
  87. /**
  88. * Créé une nouvelle organisation à partir des données contenues dans une OrganizationCreationRequest.
  89. *
  90. * @throws TransportExceptionInterface
  91. * @throws \Throwable
  92. */
  93. public function create(OrganizationCreationRequest $organizationCreationRequest): Organization
  94. {
  95. $this->logger->info(
  96. "Start the creation of a new organization named '".$organizationCreationRequest->getName()."'"
  97. );
  98. $this->entityManager->beginTransaction();
  99. try {
  100. // On vérifie si cette organisation n'existe pas déjà
  101. $this->interruptIfOrganizationExists($organizationCreationRequest);
  102. // On vérifie la validité et la disponibilité du sous domaine
  103. $this->validateSubdomain($organizationCreationRequest->getSubdomain());
  104. $this->logger->info("Subdomain is valid and available : '".$organizationCreationRequest->getSubdomain()."'");
  105. // On construit l'organisation et ses relations
  106. $organization = $this->makeOrganizationWithRelations($organizationCreationRequest);
  107. $this->logger->info('Organization created with all its relations');
  108. // On persiste et on commit, les objets liés seront persistés en cascade
  109. $this->entityManager->persist($organization);
  110. $this->entityManager->flush();
  111. $this->entityManager->commit();
  112. $this->logger->debug(' - New entities committed in DB');
  113. $this->logger->info('Organization persisted in the DB');
  114. } catch (\Throwable $e) {
  115. $this->logger->critical("An error happened, operation cancelled\n".$e);
  116. $this->entityManager->rollback();
  117. throw $e;
  118. }
  119. $withError = false;
  120. // Création de la société Dolibarr
  121. try {
  122. $dolibarrId = $this->dolibarrApiService->createSociety(
  123. $organization,
  124. $organizationCreationRequest->isClient()
  125. );
  126. $this->logger->info('New dolibarr structure created (uid : '.$dolibarrId.')');
  127. } catch (\Throwable $e) {
  128. $this->logger->critical('An error happened while creating the dolibarr society, please proceed manually.');
  129. $this->logger->debug((string) $e);
  130. $withError = true;
  131. }
  132. // Register the subdomain into the BindFile (takes up to 5min to take effect)
  133. try {
  134. $this->bindFileService->registerSubdomain($organizationCreationRequest->getSubdomain());
  135. $this->logger->info('Subdomain registered');
  136. } catch (\Throwable $e) {
  137. $this->logger->critical('An error happened while updating the bind file, please proceed manually.');
  138. $this->logger->debug((string) $e);
  139. $withError = true;
  140. }
  141. // Création du site typo3 (on est obligé d'attendre que l'organisation soit persistée en base)
  142. if ($organizationCreationRequest->getCreateWebsite()) {
  143. try {
  144. $rootUid = $this->createTypo3Website($organization);
  145. $this->logger->info('Typo3 website created (root uid: '.$rootUid.')');
  146. } catch (\Throwable $e) {
  147. $this->logger->critical('An error happened while creating the typo3 website, please proceed manually.');
  148. $this->logger->debug((string) $e);
  149. $withError = true;
  150. }
  151. } else {
  152. $this->logger->warning('Typo3 website creation was not required');
  153. }
  154. // Création de l'organisation dans la base adminassos (géré par la V1)
  155. try {
  156. $this->updateAdminassosDb($organization);
  157. $this->logger->info('Adminassos db updated');
  158. } catch (\Throwable $e) {
  159. $this->logger->critical('An error happened while updating the adminassos db, please proceed manually.');
  160. $this->logger->debug((string) $e);
  161. $withError = true;
  162. }
  163. if ($withError) {
  164. $organizationCreationRequest->setStatus(OrganizationCreationRequest::STATUS_OK_WITH_ERRORS);
  165. $this->logger->warning('-- Operation ended with errors, check the logs for more information --');
  166. } else {
  167. $organizationCreationRequest->setStatus(OrganizationCreationRequest::STATUS_OK);
  168. $this->logger->info('The organization has been created (id='.$organization->getId().').');
  169. }
  170. return $organization;
  171. }
  172. /**
  173. * Lève une exception si cette organisation existe déjà.
  174. */
  175. public function interruptIfOrganizationExists(OrganizationCreationRequest $organizationCreationRequest): void
  176. {
  177. if (
  178. $organizationCreationRequest->getSiretNumber()
  179. && $this->organizationIdentificationRepository->findOneBy(
  180. ['siretNumber' => $organizationCreationRequest->getSiretNumber()]
  181. )
  182. ) {
  183. throw new \RuntimeException("This siret number is already registered : '".$organizationCreationRequest->getSiretNumber()."'");
  184. }
  185. if (
  186. $organizationCreationRequest->getWaldecNumber()
  187. && $this->organizationIdentificationRepository->findOneBy(
  188. ['waldecNumber' => $organizationCreationRequest->getWaldecNumber()]
  189. )
  190. ) {
  191. throw new \RuntimeException("This RNA identifier (waldec number) is already registered : '".$organizationCreationRequest->getWaldecNumber()."'");
  192. }
  193. if (
  194. $organizationCreationRequest->getIdentifier()
  195. && $this->organizationIdentificationRepository->findOneBy(
  196. ['identifier' => $organizationCreationRequest->getIdentifier()]
  197. )
  198. ) {
  199. throw new \RuntimeException("This CMF identifier is already registered : '".$organizationCreationRequest->getIdentifier()."'");
  200. }
  201. $normalizedName = $this->normalizeIdentificationField($organizationCreationRequest->getName());
  202. if (
  203. $this->organizationIdentificationRepository->findOneBy(
  204. ['normalizedName' => $normalizedName, 'addressCity' => $organizationCreationRequest->getCity()]
  205. )
  206. ) {
  207. throw new \RuntimeException("An organization named '".$organizationCreationRequest->getName()."' already exists in ".$organizationCreationRequest->getCity());
  208. }
  209. $address = $this->normalizeIdentificationField(implode(' ', [
  210. $organizationCreationRequest->getStreetAddress1(),
  211. $organizationCreationRequest->getStreetAddress2(),
  212. $organizationCreationRequest->getStreetAddress3(),
  213. ]));
  214. if (
  215. $this->organizationIdentificationRepository->findOneBy(
  216. [
  217. 'normalizedAddress' => $address,
  218. 'addressCity' => $organizationCreationRequest->getCity(),
  219. 'postalCode' => $organizationCreationRequest->getPostalCode(),
  220. ]
  221. )
  222. ) {
  223. throw new \RuntimeException('An organization already exists at this address.');
  224. }
  225. }
  226. /**
  227. * Vérifie la disponibilité et la validité d'un sous domaine.
  228. *
  229. * @throws \Exception
  230. */
  231. protected function validateSubdomain(string $subdomainValue): void
  232. {
  233. if (!$this->subdomainService->isValidSubdomain($subdomainValue)) {
  234. throw new \RuntimeException('Not a valid subdomain : '.$subdomainValue);
  235. }
  236. if ($this->subdomainService->isReservedSubdomain($subdomainValue)) {
  237. throw new \RuntimeException('This subdomain is not available : '.$subdomainValue);
  238. }
  239. if ($this->subdomainService->isRegistered($subdomainValue)) {
  240. throw new \RuntimeException('This subdomain is already registered : '.$subdomainValue);
  241. }
  242. }
  243. /**
  244. * Créé une nouvelle instance d'organisation, et toutes les instances liées (paramètres, contact, adresses, ...),
  245. * selon le contenu de la requête de création.
  246. *
  247. * @throws \Throwable
  248. */
  249. protected function makeOrganizationWithRelations(
  250. OrganizationCreationRequest $organizationCreationRequest,
  251. ): Organization {
  252. // Création de l'organisation
  253. $organization = $this->makeOrganization($organizationCreationRequest);
  254. $this->logger->debug(' - Organization created');
  255. // Création des Parameters
  256. $parameters = $this->makeParameters($organizationCreationRequest);
  257. $organization->setParameters($parameters);
  258. $this->logger->debug(' - Parameters created');
  259. // Création des Settings
  260. $settings = $this->makeSettings($organizationCreationRequest);
  261. $organization->setSettings($settings);
  262. $this->logger->debug(' - Settings created');
  263. // Création de l'adresse postale
  264. $organizationAddressPostal = $this->makePostalAddress($organizationCreationRequest);
  265. $organization->addOrganizationAddressPostal($organizationAddressPostal);
  266. $this->logger->debug(' - OrganizationAddressPostal created');
  267. // Création du point de contact
  268. $contactPoint = $this->makeContactPoint($organizationCreationRequest);
  269. $organization->addContactPoint($contactPoint);
  270. $this->logger->debug(' - ContactPoint created');
  271. // Rattachement au réseau
  272. $networkOrganization = $this->makeNetworkOrganization($organizationCreationRequest);
  273. $organization->addNetworkOrganization($networkOrganization);
  274. $this->logger->debug(' - NetworkOrganization created');
  275. // Créé l'admin
  276. $adminAccess = $this->makeAdminAccess($organizationCreationRequest);
  277. $organization->addAccess($adminAccess);
  278. $this->logger->debug(' - Admin access created');
  279. // Création des cycles
  280. foreach ($this->makeCycles() as $cycle) {
  281. $organization->addCycle($cycle);
  282. }
  283. $this->logger->debug(' - Cycles created');
  284. // Création du président (si renseigné)
  285. $presidentCreationRequest = $organizationCreationRequest->getPresident();
  286. if ($presidentCreationRequest !== null) {
  287. $presidentAccess = $this->makeAccess(
  288. $presidentCreationRequest,
  289. FunctionEnum::PRESIDENT,
  290. $organizationCreationRequest->getCreationDate(),
  291. $organizationCreationRequest->getAuthorId()
  292. );
  293. $organization->addAccess($presidentAccess);
  294. $this->logger->debug(' - President access created');
  295. }
  296. // Création du directeur (si renseigné)
  297. $directorCreationRequest = $organizationCreationRequest->getDirector();
  298. if ($directorCreationRequest !== null) {
  299. $directorAccess = $this->makeAccess(
  300. $directorCreationRequest,
  301. FunctionEnum::DIRECTOR,
  302. $organizationCreationRequest->getCreationDate(),
  303. $organizationCreationRequest->getAuthorId()
  304. );
  305. $organization->addAccess($directorAccess);
  306. $this->logger->debug(' - Director access created');
  307. }
  308. // Création du sous-domaine
  309. $subdomain = $this->makeSubdomain($organizationCreationRequest);
  310. $organization->addSubdomain($subdomain);
  311. // <--- Pour la rétrocompatibilité avec la v1 ; pourra être supprimé lorsque la migration sera achevée
  312. $parameters = $organization->getParameters();
  313. $parameters->setSubDomain($organizationCreationRequest->getSubdomain());
  314. $parameters->setOtherWebsite('https://'.$organizationCreationRequest->getSubdomain().'.opentalent.fr');
  315. // --->
  316. $this->logger->debug(' - Subdomain created');
  317. return $organization;
  318. }
  319. /**
  320. * Créé une nouvelle instance d'organisation.
  321. */
  322. protected function makeOrganization(OrganizationCreationRequest $organizationCreationRequest): Organization
  323. {
  324. // Création de l'organisation
  325. $organization = new Organization();
  326. $organization->setName($organizationCreationRequest->getName());
  327. $organization->setLegalStatus($organizationCreationRequest->getLegalStatus());
  328. $organization->setPrincipalType($organizationCreationRequest->getPrincipalType());
  329. $organization->setIdentifier($organizationCreationRequest->getIdentifier());
  330. $organization->setCreationDate($organizationCreationRequest->getCreationDate());
  331. $organization->setCreateDate($organizationCreationRequest->getCreationDate());
  332. $organization->setCreatedBy($organizationCreationRequest->getAuthorId());
  333. return $organization;
  334. }
  335. /**
  336. * Create a new Parameters object from the data in an OrganizationCreationRequest.
  337. *
  338. * @param OrganizationCreationRequest $organizationCreationRequest The organization creation request
  339. *
  340. * @return Parameters The created Parameters object
  341. *
  342. * @throws \Throwable If there is an error
  343. */
  344. protected function makeParameters(OrganizationCreationRequest $organizationCreationRequest): Parameters
  345. {
  346. $parameters = new Parameters();
  347. return $parameters;
  348. }
  349. /**
  350. * Creates a new instance of the Settings class based on the given OrganizationCreationRequest object.
  351. *
  352. * @param OrganizationCreationRequest $organizationCreationRequest the OrganizationCreationRequest object containing the required data
  353. *
  354. * @return Settings the newly created instance of the Settings class
  355. */
  356. protected function makeSettings(OrganizationCreationRequest $organizationCreationRequest): Settings
  357. {
  358. $settings = new Settings();
  359. $settings->setProduct($organizationCreationRequest->getProduct());
  360. // TODO: à revoir, pour étendre à d'autres pays (voir à remplacer le champs 'country' par un champs 'currency'?)
  361. $settings->setCountry(
  362. $organizationCreationRequest->getCountryId() === 41 ? 'SWITZERLAND' : 'FRANCE'
  363. );
  364. $settings->setCreateDate($organizationCreationRequest->getCreationDate());
  365. $settings->setCreatedBy($organizationCreationRequest->getAuthorId());
  366. return $settings;
  367. }
  368. /**
  369. * Creates a new instance of the OrganizationAddressPostal class based on the given OrganizationCreationRequest object.
  370. *
  371. * @param OrganizationCreationRequest $organizationCreationRequest the OrganizationCreationRequest object containing the required data
  372. *
  373. * @return OrganizationAddressPostal the newly created instance of the OrganizationAddressPostal class
  374. */
  375. protected function makePostalAddress(OrganizationCreationRequest $organizationCreationRequest): OrganizationAddressPostal
  376. {
  377. $country = $this->countryRepository->find($organizationCreationRequest->getCountryId());
  378. if (!$country) {
  379. throw new \RuntimeException('No country found for id '.$organizationCreationRequest->getCountryId());
  380. }
  381. $addressPostal = new AddressPostal();
  382. $addressPostal->setStreetAddress($organizationCreationRequest->getStreetAddress1());
  383. $addressPostal->setStreetAddressSecond($organizationCreationRequest->getStreetAddress2());
  384. $addressPostal->setStreetAddressThird($organizationCreationRequest->getStreetAddress3());
  385. $addressPostal->setPostalCode($organizationCreationRequest->getPostalCode());
  386. $addressPostal->setAddressCity($organizationCreationRequest->getCity());
  387. $addressPostal->setAddressCountry($country);
  388. $addressPostal->setCreateDate($organizationCreationRequest->getCreationDate());
  389. $addressPostal->setCreatedBy($organizationCreationRequest->getAuthorId());
  390. $organizationAddressPostal = new OrganizationAddressPostal();
  391. $organizationAddressPostal->setAddressPostal($addressPostal);
  392. $organizationAddressPostal->setType(AddressPostalOrganizationTypeEnum::ADDRESS_HEAD_OFFICE);
  393. $organizationAddressPostal->setCreateDate($organizationCreationRequest->getCreationDate());
  394. $organizationAddressPostal->setCreatedBy($organizationCreationRequest->getAuthorId());
  395. return $organizationAddressPostal;
  396. }
  397. /**
  398. * Creates a new instance of the ContactPoint class based on the given OrganizationCreationRequest object.
  399. *
  400. * @param OrganizationCreationRequest $organizationCreationRequest the OrganizationCreationRequest object containing the required data
  401. *
  402. * @return ContactPoint the newly created instance of the ContactPoint class
  403. *
  404. * @throws NumberParseException
  405. */
  406. protected function makeContactPoint(OrganizationCreationRequest $organizationCreationRequest): ContactPoint
  407. {
  408. if (!$this->phoneNumberUtil->isPossibleNumber($organizationCreationRequest->getPhoneNumber())) {
  409. throw new \RuntimeException('Phone number is invalid or missing');
  410. }
  411. $phoneNumber = $this->phoneNumberUtil->parse($organizationCreationRequest->getPhoneNumber());
  412. $contactPoint = new ContactPoint();
  413. $contactPoint->setContactType(ContactPointTypeEnum::PRINCIPAL);
  414. $contactPoint->setEmail($organizationCreationRequest->getEmail());
  415. $contactPoint->setTelphone($phoneNumber);
  416. $contactPoint->setCreateDate($organizationCreationRequest->getCreationDate());
  417. $contactPoint->setCreatedBy($organizationCreationRequest->getAuthorId());
  418. return $contactPoint;
  419. }
  420. /**
  421. * Creates a new instance of the NetworkOrganization class based on the given OrganizationCreationRequest object.
  422. *
  423. * @param OrganizationCreationRequest $organizationCreationRequest the OrganizationCreationRequest object containing the required data
  424. *
  425. * @return NetworkOrganization the newly created instance of the NetworkOrganization class
  426. *
  427. * @throws \RuntimeException|\Exception if no parent organization is found for the given parent ID or if no network is found for the given network ID
  428. */
  429. protected function makeNetworkOrganization(OrganizationCreationRequest $organizationCreationRequest): NetworkOrganization
  430. {
  431. $parent = $this->organizationRepository->find($organizationCreationRequest->getParentId());
  432. if (!$parent) {
  433. throw new \RuntimeException('No parent organization found for id '.$organizationCreationRequest->getParentId());
  434. }
  435. if (!in_array($parent->getSettings()->getProduct(), [SettingsProductEnum::MANAGER, SettingsProductEnum::MANAGER_PREMIUM])) {
  436. throw new \RuntimeException("Parent organization must have the product 'manager' (actual product: '".$parent->getSettings()->getProduct()->value."')");
  437. }
  438. $networkOrganization = $this->organizationUtils->getActiveNetworkOrganization($parent);
  439. if (!$networkOrganization) {
  440. throw new \RuntimeException('No network found for parent '.$organizationCreationRequest->getParentId());
  441. }
  442. $network = $networkOrganization->getNetwork();
  443. // Si réseau CMF, on vérifie que le matricule est valide
  444. if ($network->getId() === NetworkEnum::CMF->value) {
  445. if (!preg_match("/FR\d{3}\w\d{8}/", $organizationCreationRequest->getIdentifier())) {
  446. throw new \RuntimeException('CMF identifier is missing or invalid.');
  447. }
  448. }
  449. $networkOrganization = new NetworkOrganization();
  450. $networkOrganization->setParent($parent);
  451. $networkOrganization->setNetwork($network);
  452. $networkOrganization->setStartDate(DatesUtils::new());
  453. $networkOrganization->setCreateDate($organizationCreationRequest->getCreationDate());
  454. $networkOrganization->setCreatedBy($organizationCreationRequest->getAuthorId());
  455. return $networkOrganization;
  456. }
  457. /**
  458. * Creates a new instance of the Access class with admin access based on the given OrganizationCreationRequest object.
  459. *
  460. * @param OrganizationCreationRequest $organizationCreationRequest the OrganizationCreationRequest object containing the required data
  461. *
  462. * @return Access the newly created instance of the Access class with admin access
  463. */
  464. protected function makeAdminAccess(OrganizationCreationRequest $organizationCreationRequest): Access
  465. {
  466. $admin = new Person();
  467. $admin->setUsername('admin'.strtolower($organizationCreationRequest->getSubdomain()));
  468. $randomString = ByteString::fromRandom(32)->toString();
  469. $admin->setPassword($randomString);
  470. $admin->setEnabled(true);
  471. $adminAccess = new Access();
  472. $adminAccess->setAdminAccess(true);
  473. $adminAccess->setPerson($admin);
  474. $adminAccess->setLoginEnabled(true);
  475. $adminAccess->setRoles(['ROLE_ADMIN', 'ROLE_ADMIN_CORE']);
  476. $adminAccess->setCreateDate($organizationCreationRequest->getCreationDate());
  477. $adminAccess->setCreatedBy($organizationCreationRequest->getAuthorId());
  478. $contactPoint = new ContactPoint();
  479. $contactPoint->setContactType(ContactPointTypeEnum::PRINCIPAL);
  480. $contactPoint->setEmail($organizationCreationRequest->getEmail());
  481. $admin->addContactPoint($contactPoint);
  482. return $adminAccess;
  483. }
  484. /**
  485. * Creates an array of Cycle objects based on a predefined set of data.
  486. *
  487. * @return Cycle[] an array of Cycle objects
  488. */
  489. protected function makeCycles(): array
  490. {
  491. $cyclesData = [
  492. ['Cycle initiation', 10, CycleEnum::INITIATION_CYCLE],
  493. ['Cycle 1', 20, CycleEnum::CYCLE_1],
  494. ['Cycle 2', 30, CycleEnum::CYCLE_2],
  495. ['Cycle 3', 40, CycleEnum::CYCLE_3],
  496. ['Cycle 4', 50, CycleEnum::CYCLE_4],
  497. ['Hors cycle', 60, CycleEnum::OUT_CYCLE],
  498. ];
  499. $cycles = [];
  500. foreach ($cyclesData as $cycleData) {
  501. $cycle = new Cycle();
  502. $cycle->setLabel($cycleData[0]);
  503. $cycle->setOrder($cycleData[1]);
  504. $cycle->setCycleEnum($cycleData[2]);
  505. $cycle->setIsSystem(false);
  506. $cycles[] = $cycle;
  507. }
  508. return $cycles;
  509. }
  510. /**
  511. * Creates an Access object based on the given OrganizationMemberCreationRequest.
  512. *
  513. * @param int|OrganizationMemberCreationRequest $creationRequestData the request object containing the
  514. * necessary data for creating a Person object,
  515. * or the id of an existing one
  516. *
  517. * @return Access the created Access object
  518. *
  519. * @throws NumberParseException
  520. */
  521. protected function makeAccess(
  522. int|OrganizationMemberCreationRequest $creationRequestData,
  523. FunctionEnum $function,
  524. \DateTime $creationDate,
  525. ?int $authorId,
  526. ): Access {
  527. if (is_int($creationRequestData)) {
  528. $person = $this->personRepository->find($creationRequestData);
  529. } else {
  530. $person = new Person();
  531. if (
  532. $creationRequestData->getUsername() !== null
  533. && $this->personRepository->findOneBy(['username' => $creationRequestData->getUsername()])
  534. ) {
  535. throw new \RuntimeException('Username already in use : '.$creationRequestData->getUsername());
  536. }
  537. $person->setUsername($creationRequestData->getUsername());
  538. $person->setPassword(ByteString::fromRandom(32)->toString());
  539. $person->setGender($creationRequestData->getGender());
  540. $person->setName(
  541. ucfirst(strtolower($creationRequestData->getName()))
  542. );
  543. $person->setGivenName(
  544. ucfirst(strtolower($creationRequestData->getGivenName()))
  545. );
  546. $personPostalAddress = $this->makePersonPostalAddress($creationRequestData, $creationDate, $authorId);
  547. $person->addPersonAddressPostal($personPostalAddress);
  548. $contactPoint = $this->makePersonContactPoint($creationRequestData, $creationDate, $authorId);
  549. $person->addContactPoint($contactPoint);
  550. $person->setCreateDate($creationDate);
  551. $person->setCreatedBy($authorId);
  552. }
  553. $access = new Access();
  554. $access->setPerson($person);
  555. $functionType = $this->functionTypeRepository->findOneBy(['mission' => $function]);
  556. $organizationFunction = new OrganizationFunction();
  557. $organizationFunction->setFunctionType($functionType);
  558. $organizationFunction->setStartDate($creationDate);
  559. $organizationFunction->setCreateDate($creationDate);
  560. $organizationFunction->setCreatedBy($authorId);
  561. $access->addOrganizationFunction($organizationFunction);
  562. $access->setCreateDate($creationDate);
  563. $access->setCreatedBy($authorId);
  564. return $access;
  565. }
  566. /**
  567. * Creates a PersonAddressPostal object based on the given OrganizationMemberCreationRequest.
  568. *
  569. * @param OrganizationMemberCreationRequest $organizationMemberCreationRequest the request object containing the
  570. * necessary data for creating a
  571. * PersonAddressPostal object
  572. *
  573. * @return PersonAddressPostal the created PersonAddressPostal object
  574. */
  575. protected function makePersonPostalAddress(
  576. OrganizationMemberCreationRequest $organizationMemberCreationRequest,
  577. \DateTime $creationDate,
  578. ?int $authorId,
  579. ): PersonAddressPostal {
  580. $addressPostal = new AddressPostal();
  581. $addressPostal->setStreetAddress($organizationMemberCreationRequest->getStreetAddress1());
  582. $addressPostal->setStreetAddressSecond($organizationMemberCreationRequest->getStreetAddress2());
  583. $addressPostal->setStreetAddressThird($organizationMemberCreationRequest->getStreetAddress3());
  584. $addressPostal->setPostalCode($organizationMemberCreationRequest->getPostalCode());
  585. $addressPostal->setAddressCity($organizationMemberCreationRequest->getCity());
  586. $addressPostal->setCreateDate($creationDate);
  587. $addressPostal->setCreatedBy($authorId);
  588. $country = $this->countryRepository->find($organizationMemberCreationRequest->getCountryId());
  589. $addressPostal->setAddressCountry($country);
  590. $personAddressPostal = new PersonAddressPostal();
  591. $personAddressPostal->setAddressPostal($addressPostal);
  592. $personAddressPostal->setType(AddressPostalPersonTypeEnum::ADDRESS_PRINCIPAL);
  593. $personAddressPostal->setCreateDate($creationDate);
  594. $personAddressPostal->setCreatedBy($authorId);
  595. return $personAddressPostal;
  596. }
  597. /**
  598. * Creates a new instance of the ContactPoint class based on the given OrganizationCreationRequest object.
  599. *
  600. * @param OrganizationMemberCreationRequest $organizationMemberCreationRequest the OrganizationMemberCreationRequest object containing the required data
  601. *
  602. * @return ContactPoint the newly created instance of the ContactPoint class
  603. *
  604. * @throws NumberParseException
  605. */
  606. protected function makePersonContactPoint(
  607. OrganizationMemberCreationRequest $organizationMemberCreationRequest,
  608. \DateTime $creationDate,
  609. ?int $authorId,
  610. ): ContactPoint {
  611. if (
  612. $organizationMemberCreationRequest->getPhone() !== null
  613. && !$this->phoneNumberUtil->isPossibleNumber($organizationMemberCreationRequest->getPhone())
  614. ) {
  615. throw new \RuntimeException('Phone number is invalid or missing (person: '.$organizationMemberCreationRequest->getUsername().')');
  616. }
  617. if (
  618. $organizationMemberCreationRequest->getMobile() !== null
  619. && !$this->phoneNumberUtil->isPossibleNumber($organizationMemberCreationRequest->getMobile())) {
  620. throw new \RuntimeException('Mobile phone number is invalid (person: '.$organizationMemberCreationRequest->getUsername().')');
  621. }
  622. $contactPoint = new ContactPoint();
  623. $contactPoint->setContactType(ContactPointTypeEnum::PRINCIPAL);
  624. $contactPoint->setEmail($organizationMemberCreationRequest->getEmail());
  625. if ($organizationMemberCreationRequest->getPhone() !== null) {
  626. $phoneNumber = $this->phoneNumberUtil->parse($organizationMemberCreationRequest->getPhone());
  627. $contactPoint->setTelphone($phoneNumber);
  628. }
  629. if ($organizationMemberCreationRequest->getMobile() !== null) {
  630. $mobileNumber = $this->phoneNumberUtil->parse($organizationMemberCreationRequest->getMobile());
  631. $contactPoint->setMobilPhone($mobileNumber);
  632. }
  633. $contactPoint->setCreateDate($creationDate);
  634. $contactPoint->setCreatedBy($authorId);
  635. return $contactPoint;
  636. }
  637. protected function makeSubdomain(OrganizationCreationRequest $organizationCreationRequest): Subdomain
  638. {
  639. $subdomain = new Subdomain();
  640. $subdomain->setSubdomain($organizationCreationRequest->getSubdomain());
  641. $subdomain->setActive(true);
  642. return $subdomain;
  643. }
  644. /**
  645. * Créé le site Typo3 et retourne l'id de la page racine du site nouvellement créé, ou null en cas d'erreur.
  646. *
  647. * @throws RedirectionExceptionInterface
  648. * @throws ClientExceptionInterface
  649. * @throws TransportExceptionInterface
  650. * @throws ServerExceptionInterface
  651. */
  652. protected function createTypo3Website(Organization $organization): ?int
  653. {
  654. $response = $this->typo3Service->createSite($organization->getId());
  655. $content = json_decode($response->getContent(), true);
  656. $rootPageUid = $content['root_uid'];
  657. if ($response->getStatusCode() === Response::HTTP_OK && $rootPageUid > 0) {
  658. // TODO: revoir l'utilité du champs cmsId
  659. $organization->setCmsId($rootPageUid);
  660. $this->entityManager->persist($organization);
  661. $this->entityManager->flush();
  662. return $rootPageUid;
  663. } else {
  664. $this->logger->critical("/!\ A critical error happened while creating the Typo3 website");
  665. $this->logger->debug($response->getContent());
  666. }
  667. return null;
  668. }
  669. protected function updateAdminassosDb(Organization $organization): void
  670. {
  671. $response = $this->apiLegacyRequestService->get(
  672. UrlBuilder::concatPath('/_internal/request/adminassos/create/organization/', [(string) $organization->getId()])
  673. );
  674. if ($response->getStatusCode() !== Response::HTTP_OK) {
  675. throw new \RuntimeException('An error happened while updating the adminassos database: '.$response->getContent());
  676. }
  677. }
  678. /**
  679. * Normalise la chaine comme sont normalisées les champs de l'entité OrganizationIdentification.
  680. *
  681. * @øee sql/schema-extensions/003-view_organization_identification.sql
  682. */
  683. protected function normalizeIdentificationField(string $value): string
  684. {
  685. $value = strtolower(trim($value));
  686. $value = preg_replace('/[éèê]/u', 'e', $value);
  687. $value = preg_replace('/[à]/u', 'a', $value);
  688. $value = preg_replace('/[ç]/u', 'c', $value);
  689. return preg_replace('/[^a-z0-9]+/u', '+', $value);
  690. }
  691. /**
  692. * /!\ Danger zone /!\.
  693. *
  694. * Supprime définitivement une organisation, ses données, ses fichiers, son site internet, et son profil Dolibarr.
  695. *
  696. * Pour éviter une suppression accidentelle, cette méthode ne doit pouvoir être exécutée que si la requête a été
  697. * envoyée depuis le localhost.
  698. *
  699. * @throws \Exception
  700. */
  701. public function delete(OrganizationDeletionRequest $organizationDeletionRequest): OrganizationDeletionRequest
  702. {
  703. SecurityUtils::preventIfNotLocalhost();
  704. $organization = $this->organizationRepository->find($organizationDeletionRequest->getOrganizationId());
  705. if (!$organization) {
  706. throw new \RuntimeException('No organization was found for id : '.$organizationDeletionRequest->getOrganizationId());
  707. }
  708. $this->logger->info(
  709. "Start the deletion of organization '".$organization->getName()."' [".$organization->getId().']'
  710. );
  711. $this->entityManager->beginTransaction();
  712. $withError = false;
  713. try {
  714. $orphanPersons = $this->getFutureOrphanPersons($organization);
  715. // On est obligé de supprimer manuellement les paramètres, car c'est l'entité Parameters qui est
  716. // propriétaire de la relation Organization ↔ Parameters.
  717. $this->entityManager->remove($organization->getParameters());
  718. // Toutes les autres entités liées seront supprimées en cascade
  719. $this->entityManager->remove($organization);
  720. // Supprime les personnes qui n'avaient pas d'autre Access attaché
  721. $deletedPersonIds = [];
  722. foreach ($orphanPersons as $person) {
  723. $deletedPersonIds[] = $person->getId();
  724. $this->entityManager->remove($person);
  725. }
  726. $this->entityManager->flush();
  727. $this->entityManager->commit();
  728. $this->logger->info('Organization deleted');
  729. } catch (\Exception $e) {
  730. $this->logger->critical("An error happened, operation cancelled\n".$e);
  731. $this->entityManager->rollback();
  732. throw $e;
  733. }
  734. try {
  735. $this->deleteTypo3Website($organizationDeletionRequest->getOrganizationId());
  736. $this->logger->info('Typo3 website deleted');
  737. } catch (\Exception $e) {
  738. $this->logger->critical('An error happened while deleting the Typo3 website, please proceed manually.');
  739. $this->logger->debug((string) $e);
  740. $withError = true;
  741. }
  742. try {
  743. $this->switchDolibarrSocietyToProspect($organizationDeletionRequest->getOrganizationId());
  744. $this->logger->info('Dolibarr society switched to prospect');
  745. } catch (\Exception $e) {
  746. $this->logger->critical('An error happened while updating the Dolibarr society, please proceed manually.');
  747. $this->logger->debug((string) $e);
  748. $withError = true;
  749. }
  750. try {
  751. $this->fileManager->deleteOrganizationFiles($organizationDeletionRequest->getOrganizationId());
  752. $this->logger->info('Organization files deleted');
  753. } catch (\RuntimeException $e) {
  754. // Nothing to delete
  755. } catch (\Exception $e) {
  756. $this->logger->critical("An error happened while deleting the organization's files, please proceed manually.");
  757. $this->logger->debug((string) $e);
  758. $withError = true;
  759. }
  760. foreach ($deletedPersonIds as $personId) {
  761. try {
  762. $this->fileManager->deletePersonFiles($personId);
  763. } catch (\RuntimeException $e) {
  764. // Nothing to delete
  765. } catch (\Exception $e) {
  766. $this->logger->critical("An error happened while deleting the person's files, please proceed manually (id=".$personId.').');
  767. $this->logger->debug((string) $e);
  768. $withError = true;
  769. }
  770. }
  771. $this->logger->info("Organization's persons files deleted");
  772. if ($withError) {
  773. $organizationDeletionRequest->setStatus(OrganizationDeletionRequest::STATUS_OK_WITH_ERRORS);
  774. $this->logger->warning('-- Operation ended with errors, check the logs for more information --');
  775. } else {
  776. $organizationDeletionRequest->setStatus(OrganizationDeletionRequest::STATUS_OK);
  777. }
  778. $this->logger->info(
  779. 'The organization has been deleted.'
  780. );
  781. return $organizationDeletionRequest;
  782. }
  783. /**
  784. * Supprime tous les Access d'une organisation, ainsi que la Person
  785. * rattachée (si celle-ci n'est pas liée à d'autres Access).
  786. *
  787. * @return array<Person>
  788. */
  789. protected function getFutureOrphanPersons(Organization $organization): array
  790. {
  791. $orphans = [];
  792. foreach ($organization->getAccesses() as $access) {
  793. $person = $access->getPerson();
  794. if ($person->getAccesses()->count() === 1) {
  795. $orphans[] = $person;
  796. }
  797. }
  798. return $orphans;
  799. }
  800. protected function deleteTypo3Website(int $organizationId): void
  801. {
  802. $this->typo3Service->hardDeleteSite($organizationId);
  803. }
  804. protected function switchDolibarrSocietyToProspect(int $organizationId): void
  805. {
  806. $this->dolibarrApiService->switchSocietyToProspect($organizationId);
  807. }
  808. /**
  809. * Sets the password for the admin account of an organization.
  810. *
  811. * The admin account is identified by the adminaccess property set to true.
  812. * The password must meet the complexity requirements: at least 8 characters,
  813. * including at least one uppercase letter, one lowercase letter, one digit,
  814. * and one special character.
  815. *
  816. * @param Organization $organization The organization whose admin account password will be set
  817. * @param string $password The plain password to set
  818. *
  819. * @throws \RuntimeException If no admin account is found or if the password doesn't meet the requirements
  820. */
  821. public function setAdminAccountPassword(Organization $organization, string $password): void
  822. {
  823. // Validate password complexity
  824. if (!preg_match(self::PASSWORD_PATTERN, $password)) {
  825. throw new \RuntimeException('Password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one digit, and one special character.');
  826. }
  827. // Find the admin access using the repository
  828. $adminAccess = $this->accessRepository->findAdminAccess($organization);
  829. if (!$adminAccess) {
  830. throw new \RuntimeException('No admin account found for this organization.');
  831. }
  832. // Set the password on the Person entity
  833. $person = $adminAccess->getPerson();
  834. $person->setPassword($password);
  835. // Persist the changes
  836. $this->entityManager->persist($person);
  837. $this->entityManager->flush();
  838. $this->logger->info('Admin account password set for organization: ' . $organization->getId());
  839. }
  840. }