OrganizationFactory.php 39 KB

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