Explorar o código

finalize shop order for new structure premium trial

Olivier Massot hai 6 meses
pai
achega
93fbf35345

+ 1 - 0
.env

@@ -23,6 +23,7 @@ APP_SECRET=6a76497c8658bb23e2236f97a2627df3
 DATABASE_URL=xxx
 DATABASE_ADMINASSOS_URL=xxx
 DATABASE_AUDIT_URL=xxx
+DATABASE_DOLIBARR_URL=xxx
 DOLIBARR_API_TOKEN=xxx
 MERCURE_JWT_SECRET=xxx
 ###< secret values ###

+ 6 - 6
config/packages/docker/messenger.yaml

@@ -1,9 +1,9 @@
 # Désactive le fonctionnement asynchrone de messenger en mode dev
 # > commenter pour tester avec un fonctionnement asynchrone
 #   (dans ce cas, la commande `messenger:consume async` doit être en cours d'exécution)
-#framework:
-#    messenger:
-#        transports:
-#            # https://symfony.com/doc/current/messenger.html#transport-configuration
-#            async: 'sync://'
-#            failed: 'sync://'
+framework:
+    messenger:
+        transports:
+            # https://symfony.com/doc/current/messenger.html#transport-configuration
+            async: 'sync://'
+            failed: 'sync://'

+ 4 - 2
config/packages/doctrine.yaml

@@ -17,10 +17,12 @@ doctrine:
 
             adminassos:
                 url: '%env(resolve:DATABASE_ADMINASSOS_URL)%'
+                server_version: '5.7'
 
-                # IMPORTANT: You MUST configure your server version,
-                # either here or in the DATABASE_URL env var (see .env file)
+            dolibarr:
+                url: '%env(resolve:DATABASE_DOLIBARR_URL)%'
                 server_version: '5.7'
+
         types:
             uuid: Ramsey\Uuid\Doctrine\UuidType
 

+ 3 - 0
config/secrets/docker/docker.DATABASE_DOLIBARR_URL.76f96a.php

@@ -0,0 +1,3 @@
+<?php // docker.DATABASE_DOLIBARR_URL.76f96a on Thu, 19 Jun 2025 09:06:57 +0000
+
+return "gw\xD4\x05\x0E\xED\xBD2\xB5\xE7I\x97\x02\x237\x1DX\xBA\x5C\x04\x5BePi\xB3dl\xFD\xD0\x273J\x24\xE3\xC7Y\x28\xC9\xDE\xD0K\xB0\xC7\xF2\xD1E\x1E\xB9\xF2\xC0\xDB25\x5D\x89S\x3A\x02\x7C\xA6\xC0\x3B\xA6\xA3\xD4\x01I\xA8\x16G\xB4l\x8DzE\xD5\xFCuq\x84\xE0n\x241q\x18\x3C\xAA\xDC\x84\xB0\x17\x0B\xF5\xD1o\xF9\x02\x8C\xA6\x9CJ\x05\xF1\xA4\x86R\xBD\xBB\xC3i\x935\x1E\x18E\x176\x18\x91\x40\xEB\xD1\x19v\x24\xF0\x17\x23h\x18\xC8\xFB\xD7\x60yY\xEA\x8F\x04\x8F\xB8\xE9Z\xDB2a\xEB5\xAE\xF7\x7C\x92\xA3\xFF";

+ 1 - 0
config/secrets/docker/docker.list.php

@@ -3,6 +3,7 @@
 return [
     'DATABASE_ADMINASSOS_URL' => null,
     'DATABASE_AUDIT_URL' => null,
+    'DATABASE_DOLIBARR_URL' => null,
     'DATABASE_URL' => null,
     'DOLIBARR_API_TOKEN' => null,
     'MERCURE_JWT_SECRET' => null,

+ 33 - 12
src/ApiResources/Shop/NewStructureArtistPremiumTrialRequest.php

@@ -17,7 +17,10 @@ use Symfony\Component\Validator\Constraints as Assert;
  */
 #[ApiResource(
     operations: [
-        new Post(processor: NewStructureArtistPremiumTrialRequestProcessor::class),
+        new Post(
+            uriTemplate: '/public/shop/new-structure-artist-premium-trial-request',
+            processor: NewStructureArtistPremiumTrialRequestProcessor::class
+        ),
     ]
 )]
 class NewStructureArtistPremiumTrialRequest
@@ -29,7 +32,10 @@ class NewStructureArtistPremiumTrialRequest
     #[ApiProperty(identifier: true)]
     private int $id = 0;
 
-    #[Assert\NotBlank]
+    #[Assert\Length(
+        min: 2,
+        minMessage: "Structure's name must be at least {{ limit }} characters long",
+    )]
     private string $structureName;
 
     #[Assert\NotBlank]
@@ -37,40 +43,55 @@ class NewStructureArtistPremiumTrialRequest
 
     private ?string $addressComplement = null;
 
-    #[Assert\NotBlank]
+    #[Assert\Length(
+        min: 3,
+        minMessage: 'Postal code must be at least {{ limit }} characters long',
+    )]
     private string $postalCode;
 
-    #[Assert\NotBlank]
+    #[Assert\Length(
+        min: 1,
+        minMessage: 'City must be at least {{ limit }} characters long',
+    )]
     private string $city;
 
     #[Assert\NotBlank]
     #[Assert\Email(message: 'The email {{ value }} is not a valid email.')]
     private string $structureEmail;
 
-    #[Assert\NotBlank]
     private PrincipalTypeEnum $structureType;
 
-    #[Assert\NotBlank]
     private LegalEnum $legalStatus;
 
-    #[Assert\NotBlank]
-    #[Assert\Length(min: 9, max: 9)]
+    #[Assert\Regex(pattern: '/^|(\d{9})$/')]
     private string $siren;
 
-    #[Assert\NotBlank]
+    #[Assert\Length(
+        min: 1,
+        minMessage: 'Representative first name must be at least {{ limit }} characters long',
+    )]
     private string $representativeFirstName;
 
-    #[Assert\NotBlank]
+    #[Assert\Length(
+        min: 1,
+        minMessage: 'Representative last name must be at least {{ limit }} characters long',
+    )]
     private string $representativeLastName;
 
-    #[Assert\NotBlank]
+    #[Assert\Length(
+        min: 1,
+        minMessage: 'Representative function must be at least {{ limit }} characters long',
+    )]
     private string $representativeFunction;
 
     #[Assert\NotBlank]
     #[Assert\Email(message: 'The email {{ value }} is not a valid email.')]
     private string $representativeEmail;
 
-    #[Assert\NotBlank]
+    #[Assert\Length(
+        min: 10,
+        minMessage: 'Phone number must be at least {{ limit }} characters long',
+    )]
     private string $representativePhone;
 
     #[Assert\IsTrue(message: 'terms-must-be-accepted')]

+ 1 - 1
src/Entity/Shop/ShopRequest.php

@@ -21,7 +21,7 @@ use Symfony\Component\Validator\Constraints as Assert;
  */
 #[ApiResource(operations: [
     new Get(
-        uriTemplate: '/shop/validate/{token}',
+        uriTemplate: '/public/shop/validate/{token}',
         provider: ShopRequestProvider::class,
     )
 ])]

+ 17 - 0
src/Enum/Access/AccessIdsEnum.php

@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Enum\Access;
+
+use App\Enum\EnumMethodsTrait;
+
+/**
+ * Id de comptes spécifiques.
+ */
+enum AccessIdsEnum: int
+{
+    use EnumMethodsTrait;
+
+    case ADMIN_2IOPENSERVICE = 10984;
+}

+ 4 - 76
src/Message/Handler/Shop/NewStructureArtistPremiumTrialHandler.php

@@ -19,14 +19,11 @@ use Symfony\Component\Messenger\Attribute\AsMessageHandler;
 use Symfony\Component\Serializer\SerializerInterface;
 
 #[AsMessageHandler]
-class NewStructureArtistPremiumTrialHandler
+readonly class NewStructureArtistPremiumTrialHandler
 {
     public function __construct(
-        private readonly EntityManagerInterface $entityManager,
-        private readonly OrganizationFactory $organizationFactory,
-        private readonly ShopService $shopService,
-        private readonly SerializerInterface $serializer,
-        private readonly LoggerInterface $logger
+        private ShopService            $shopService,
+
     ) {
     }
 
@@ -34,76 +31,7 @@ class NewStructureArtistPremiumTrialHandler
     {
         $token = $message->getToken();
 
-        // Retrieve the ShopRequest entity using its token
-        $shopRequest = $this->entityManager->find(ShopRequest::class, $token);
-
-        if (!$shopRequest) {
-            $this->logger->error('Cannot find ShopRequest with token: ' . $token);
-            return;
-        }
-
-        try {
-            // Convert the stored JSON data to a NewStructureArtistPremiumTrialRequest object
-            $data = $shopRequest->getData();
-            $trialRequest = $this->serializer->deserialize(
-                json_encode($data),
-                NewStructureArtistPremiumTrialRequest::class,
-                'json'
-            );
-
-            // Generate an OrganizationCreationRequest object
-            $organizationCreationRequest = new OrganizationCreationRequest();
-            $organizationCreationRequest->setName($trialRequest->getStructureName());
-            $organizationCreationRequest->setStreetAddress1($trialRequest->getAddress());
-            $organizationCreationRequest->setStreetAddress2($trialRequest->getAddressComplement());
-            $organizationCreationRequest->setPostalCode($trialRequest->getPostalCode());
-            $organizationCreationRequest->setCity($trialRequest->getCity());
-            $organizationCreationRequest->setEmail($trialRequest->getStructureEmail());
-            $organizationCreationRequest->setPrincipalType($trialRequest->getStructureType());
-            $organizationCreationRequest->setLegalStatus($trialRequest->getLegalStatus());
-            $organizationCreationRequest->setSiretNumber($trialRequest->getSiren());
-
-            // Generate a subdomain from the structure name
-            $subdomain = $this->generateSubdomain($trialRequest->getStructureName());
-            $organizationCreationRequest->setSubdomain($subdomain);
-
-            // Set default values
-            $organizationCreationRequest->setProduct(SettingsProductEnum::ARTIST_PREMIUM);
-            $organizationCreationRequest->setCountryId(41); // Default country ID
-            $organizationCreationRequest->setParentId(1); // Default parent ID
-            $organizationCreationRequest->setCreateWebsite(true);
-            $organizationCreationRequest->setCreationDate(new \DateTime());
-
-            // Create the organization
-            $organization = $this->organizationFactory->create($organizationCreationRequest);
-
-            // Start the artist premium trial
-            $this->shopService->startArtistPremiumTrial($organization, $trialRequest);
-
-            $this->logger->info('Successfully processed NewStructureArtistPremiumTrial for token: ' . $token);
-        } catch (\Exception $e) {
-            $this->logger->error('Error processing NewStructureArtistPremiumTrial: ' . $e->getMessage());
-        }
+        $this->shopService->handleNewStructureArtistPremiumTrialRequest($token);
     }
 
-    /**
-     * Generate a subdomain from a structure name.
-     */
-    private function generateSubdomain(string $name): string
-    {
-        // Remove accents and special characters
-        $subdomain = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
-        // Replace spaces and special characters with hyphens
-        $subdomain = preg_replace('/[^a-zA-Z0-9]/', '-', $subdomain);
-        // Convert to lowercase
-        $subdomain = strtolower($subdomain);
-        // Remove consecutive hyphens
-        $subdomain = preg_replace('/-+/', '-', $subdomain);
-        // Trim hyphens from beginning and end
-        $subdomain = trim($subdomain, '-');
-        // Limit length
-        $subdomain = substr($subdomain, 0, 30);
-
-        return $subdomain;
-    }
 }

+ 136 - 0
src/Service/Dolibarr/DolibarrApiService.php

@@ -6,7 +6,10 @@ namespace App\Service\Dolibarr;
 
 use App\Entity\Organization\Organization;
 use App\Enum\Dolibarr\DolibarrDocTypeEnum;
+use App\Enum\Organization\SettingsProductEnum;
 use App\Service\Rest\ApiRequestService;
+use App\Service\Utils\DatesUtils;
+use Exception;
 use JetBrains\PhpStorm\Pure;
 use Symfony\Component\HttpKernel\Exception\HttpException;
 use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
@@ -55,6 +58,19 @@ class DolibarrApiService extends ApiRequestService
         }
     }
 
+    /**
+     * Get the dolibarr id of an organization
+     *
+     * @param int $organizationId
+     * @return int|null
+     * @throws \JsonException
+     */
+    public function getSocietyId(int $organizationId): ?int
+    {
+        $society = $this->getSociety($organizationId);
+        return $society ? (int)$society['id'] : null;
+    }
+
     /**
      * Get the first active contract for the given dolibarr society.
      *
@@ -279,4 +295,124 @@ class DolibarrApiService extends ApiRequestService
 
         return $this->getJsonContent($route);
     }
+
+    /**
+     * Créé un nouveau contrat Dolibarr
+     *
+     * @param int $socId
+     * @param int $productId L'id du produit dans dolibarr (@see DolibarrUtils::getProductId())
+     * @param bool $isNewClient
+     * @param int $duration Durée du contrat (en mois)
+     * @return int
+     * @throws Exception
+     */
+    public function createContract(
+        int  $socId,
+        int  $productId,
+        bool $isNewClient = false,
+        int $duration = 12
+    ): int {
+        $route = "contracts";
+        $date = DatesUtils::new();
+
+        $product = $this->getJsonContent(
+            "products/$productId"
+        );
+
+        // Evolution (3) ou nouveau client (1)
+        $originVente = $isNewClient ? 1 : 3;
+
+        $body = [
+            "socid" => $socId,
+            "date_contrat" => $date->format('Y-m-d'),
+            "commercial_signature_id" => 8,
+            "commercial_suivi_id" => 8,
+            'statut' => 1,
+            'lines' => [
+                [
+                    'fk_product' => $productId,
+                    'label' => $product['label'],
+                    'desc' => $product['description'],
+                    'qty' => 1,
+                    'subprice' => number_format((float)$product['price'],2),
+                    'price_base_type' => $product['price_base_type'],
+                    'tva_tx' => $product['tva_tx'],
+                ]
+            ],
+            'array_options' => [
+                'options_ec_amount' => number_format((float)$product['price'],2),
+                'options_ec_duration_months' => $duration,
+                'options_ec_signature_date' => $date->format('Y-m-d'),
+                'options_ec_effective_date' => $date->format('Y-m-d'),
+                'options_ec_tacit_renewal' => 1,
+                'options_ec_termination_period_months' => 2,
+                'options_ec_billing_due' => 1,
+                'options_ec_billing_frequency' => 4,
+                'options_ec_billing_begin_period' => 1,
+                'options_ec_payment_condition' => 7, // A Livraison
+                'options_ec_payment_mode' => 6, // CB (voir table llx_c_paiement)
+                'options_ec_account' => 1,
+                'options_logicielfact' => 1,
+                'options_versionfact' => 2,
+                'options_2iopen_origvente' => $originVente,
+            ]
+        ];
+
+        return (int)$this->post($route, $body)->getContent();
+    }
+
+    /**
+     * Ajoute une ligne au contrat
+     *
+     * @param int $contractId
+     * @param int $productId
+     * @param int $duration Durée du contrat (en jours)
+     * @return int
+     * @throws Exception
+     */
+    public function createContractLine(int $contractId, int $productId, int $duration = 12): int {
+        $route = "contracts/$contractId/lines";
+
+        $date = DatesUtils::new();
+        $endDate = DatesUtils::new()->modify('+'.$duration.' months')->modify('-1 day');
+
+        $product = $this->getJsonContent(
+            "products/$productId"
+        );
+
+        $body = [
+            'fk_product' => $productId,
+            'label' => $product['label'],
+            'desc' => $product['description'],
+            'qty' => 1,
+            'subprice' => number_format((float)$product['price'],2),
+            'price_base_type' => $product['price_base_type'],
+            'tva_tx' => $product['tva_tx'],
+            'date_start' => $date->format('Y-m-d'),
+            'date_end' => $endDate->format('Y-m-d'),
+        ];
+
+        return (int)$this->post($route, $body)->getContent();
+    }
+
+    /**
+     * Met à jour le produit possédé par la structure dans Dolibarr
+     *
+     * @param int $socId
+     * @param SettingsProductEnum $contractType Produit concerné (@see SettingsProductEnum)
+     * @param bool $isTrial
+     * @return void
+     */
+    public function updateSocietyProduct(int $socId, string $productName): void
+    {
+        $route = "thirdparties/$socId";
+
+        $body = [
+            'array_options' => [
+                'options_2iopen_software_opentalent' => $productName
+            ]
+        ];
+
+        $this->put($route, $body);
+    }
 }

+ 122 - 0
src/Service/Dolibarr/DolibarrUtils.php

@@ -0,0 +1,122 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Dolibarr;
+
+use App\Enum\Organization\SettingsProductEnum;
+use App\Service\Utils\DatesUtils;
+use Doctrine\DBAL\Connection;
+use Doctrine\ORM\EntityManagerInterface;
+use Exception;
+
+class DolibarrUtils
+{
+    const ARTIST_STANDARD_CMF_PRODUCT_ID = 283;
+    const ARTIST_PREMIUM_TRIAL_PRODUCT_ID = 598;
+    const ARTIST_PREMIUM_PRODUCT_ID = 281;
+    const ARTIST_PREMIUM_CMF_PRODUCT_ID = 282;
+
+    public function __construct(
+        private Connection $dolibarrConnection,
+    ) {}
+
+    /**
+     * Retourne l'id Dolibarr du produit donné, selon le produit possédé et l'appartenance
+     * ou non au réseau CMF.
+     *
+     * @param SettingsProductEnum $contractType Produit concerné (@see SettingsProductEnum)
+     * @param bool $isTrial
+     * @param bool $isCmf
+     * @return int
+     */
+    public function getProductId(
+        SettingsProductEnum $contractType,
+        bool $isTrial = false,
+        bool $isCmf = false
+    ): int {
+        if ($contractType === SettingsProductEnum::ARTIST_PREMIUM && $isTrial) {
+            return self::ARTIST_PREMIUM_TRIAL_PRODUCT_ID;
+        } else if ($contractType === SettingsProductEnum::ARTIST_PREMIUM && $isCmf) {
+            return self::ARTIST_PREMIUM_CMF_PRODUCT_ID;
+        } else if ($contractType === SettingsProductEnum::ARTIST_PREMIUM) {
+            return self::ARTIST_PREMIUM_PRODUCT_ID;
+        } else if ($contractType === SettingsProductEnum::ARTIST && $isCmf) {
+            return self::ARTIST_STANDARD_CMF_PRODUCT_ID;
+        } else {
+            throw new \InvalidArgumentException("Invalid contract type");
+        }
+    }
+
+    /**
+     * Exécute une requête SQL sur la DB Dolibarr
+     *
+     * @throws \Doctrine\DBAL\Exception
+     */
+    protected function executeQuery(string $sql): void
+    {
+        $this->dolibarrConnection->executeQuery($sql);
+    }
+
+    /**
+     * Remplace le ou les commerciaux actuellement affectés à la société par l'utilisateur 'api'
+     * (pas de solution trouvée via l'API)
+     *
+     * @param int $societyId
+     * @return void
+     * @throws \Doctrine\DBAL\Exception
+     */
+    public function updateSocietyCommercialsWithApi(int $societyId) {
+        $apiUserId = 8;
+
+        $this->executeQuery(
+            "DELETE FROM llx_societe_commerciaux WHERE fk_soc = $societyId"
+        );
+
+        $this->executeQuery(
+            "INSERT INTO llx_societe_commerciaux (fk_soc, fk_user) 
+                   VALUES ($societyId, $apiUserId)"
+        );
+    }
+
+    /**
+     * Enregistre une entrée dans le journal des actions commercial de la société Dolibarr
+     * (pas de solution trouvée via l'API)
+     *
+     * @param int $societyId
+     * @param string $title
+     * @param string $message
+     * @return void
+     * @throws Exception
+     */
+    public function addActionComm(int $societyId, string $title, string $message): void
+    {
+        $tz = new \DateTimeZone('Europe/Paris');
+        $now = DatesUtils::new('now', $tz)->format('Y-m-d H:i:s');
+        $apiUserId = 8;
+
+        $this->executeQuery(
+            "INSERT INTO llx_actioncomm (fk_soc, ref, code, label, note, datep, datep2, datec, fk_user_author, fk_user_mod, fk_user_action, percent) 
+                   VALUES ($societyId, -1, 'AC_OT_ONLINE_STORE', '$title', '$message', '$now', '$now', '$now', $apiUserId, $apiUserId, $apiUserId, -1)"
+        );
+    }
+
+    /**
+     * Retourne le nom du produit dans Dolibarr
+     *
+     * @param SettingsProductEnum $contractType Produit concerné (@see SettingsProductEnum)
+     * @param bool $isTrial
+     * @return string|null
+     */
+    public function getDolibarrProductName(SettingsProductEnum $contractType, bool $isTrial = false): ?string
+    {
+        return match ($contractType) {
+            SettingsProductEnum::ARTIST => "Opentalent Artist",
+            SettingsProductEnum::ARTIST_PREMIUM => $isTrial ? "Opentalent Artist Premium (Essai)" : "Opentalent Artist Premium",
+            SettingsProductEnum::SCHOOL => "Opentalent School",
+            SettingsProductEnum::SCHOOL_PREMIUM => $isTrial ? "Opentalent School Premium (Essai)" : "Opentalent School Premium",
+            SettingsProductEnum::MANAGER => "Opentalent Manager",
+            SettingsProductEnum::MANAGER_PREMIUM => "Opentalent Manager Premium",
+            default => null,
+        };
+    }
+}

+ 2 - 1
src/Service/Mailer/Builder/AbstractBuilder.php

@@ -64,7 +64,8 @@ class AbstractBuilder implements AbstractBuilderInterface
      */
     public function render(string $template, array $context): string
     {
-        return $this->twig->render(sprintf('@templates/emails/%s.html.twig', $template), $context);
+        $templatePath = sprintf('@templates/emails/%s.html.twig', $template);
+        return $this->twig->render($templatePath, $context);
     }
 
     /**

+ 7 - 7
src/Service/Mailer/Builder/NewStructureTrialRequestValidationBuilder.php → src/Service/Mailer/Builder/NewStructureArtistPremiumTrialRequestValidationBuilder.php

@@ -8,14 +8,14 @@ use App\Entity\Access\Access;
 use App\Enum\Core\EmailSendingTypeEnum;
 use App\Service\Mailer\Email;
 use App\Service\Mailer\Model\MailerModelInterface;
-use App\Service\Mailer\Model\NewStructureTrialRequestValidationModel;
+use App\Service\Mailer\Model\NewStructureArtistPremiumTrialRequestValidationModel;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\ORM\EntityManagerInterface;
 
 /**
  * Classe NewStructureTrialRequestValidationBuilder qui est chargé de construire l'Email de validation d'une demande d'essai de nouvelle structure.
  */
-class NewStructureTrialRequestValidationBuilder extends AbstractBuilder implements BuilderInterface
+class NewStructureArtistPremiumTrialRequestValidationBuilder extends AbstractBuilder implements BuilderInterface
 {
     public function __construct(
         private readonly EntityManagerInterface $entityManager,
@@ -25,11 +25,11 @@ class NewStructureTrialRequestValidationBuilder extends AbstractBuilder implemen
 
     public function support(MailerModelInterface $mailerModel): bool
     {
-        return $mailerModel instanceof NewStructureTrialRequestValidationModel;
+        return $mailerModel instanceof NewStructureArtistPremiumTrialRequestValidationModel;
     }
 
     /**
-     * @param NewStructureTrialRequestValidationModel $mailerModel
+     * @param NewStructureArtistPremiumTrialRequestValidationModel $mailerModel
      */
     public function build(MailerModelInterface $mailerModel): ArrayCollection
     {
@@ -42,14 +42,14 @@ class NewStructureTrialRequestValidationBuilder extends AbstractBuilder implemen
             'structureName' => $mailerModel->getStructureName(),
             'validationUrl' => $mailerModel->getValidationUrl(),
         ];
-        $content = $this->render('shop/new-structure-validation', $context);
+
+        $content = $this->render('shop/new-structure-artist-premium-trial-validation', $context);
 
         $email = (new Email())
             ->setEmailEntity($this->buildEmailEntity('Validation de votre demande d\'essai', $author, $content))
             ->setContent($content)
             ->setFrom($this->opentalentNoReplyEmailAddress)
-            ->setFromName('OpenTalent')
-        ;
+            ->setFromName('Opentalent');
 
         // Add recipient as a string (direct email address)
         $this->addRecipient($email, $mailerModel->getRepresentativeEmail(), EmailSendingTypeEnum::TO);

+ 1 - 1
src/Service/Mailer/Model/NewStructureTrialRequestValidationModel.php → src/Service/Mailer/Model/NewStructureArtistPremiumTrialRequestValidationModel.php

@@ -7,7 +7,7 @@ namespace App\Service\Mailer\Model;
 /**
  * Classe NewStructureTrialRequestValidationModel qui conserve les données pour construire le mail de validation d'une demande d'essai de nouvelle structure.
  */
-class NewStructureTrialRequestValidationModel extends AbstractMailerModel implements MailerModelInterface
+class NewStructureArtistPremiumTrialRequestValidationModel extends AbstractMailerModel implements MailerModelInterface
 {
     private string $token;
     private string $representativeEmail;

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

@@ -41,6 +41,7 @@ use App\Service\Typo3\SubdomainService;
 use App\Service\Typo3\Typo3Service;
 use App\Service\Utils\DatesUtils;
 use App\Service\Utils\SecurityUtils;
+use App\Service\Utils\UrlBuilder;
 use Doctrine\ORM\EntityManagerInterface;
 use libphonenumber\NumberParseException;
 use libphonenumber\PhoneNumberUtil;

+ 163 - 56
src/Service/Shop/ShopService.php

@@ -7,21 +7,32 @@ namespace App\Service\Shop;
 use App\ApiResources\Organization\OrganizationCreationRequest;
 use App\ApiResources\Shop\NewStructureArtistPremiumTrialRequest;
 use App\Entity\Organization\Organization;
+use App\Entity\Organization\Subdomain;
 use App\Entity\Shop\ShopRequest;
+use App\Enum\Access\AccessIdsEnum;
+use App\Enum\Organization\SettingsProductEnum;
 use App\Enum\Shop\ShopRequestStatus;
 use App\Enum\Shop\ShopRequestType;
 use App\Message\Message\Shop\NewStructureArtistPremiumTrial;
+use App\Repository\Organization\OrganizationRepository;
 use App\Service\ApiLegacy\ApiLegacyRequestService;
+use App\Service\Dolibarr\DolibarrApiService;
+use App\Service\Dolibarr\DolibarrUtils;
 use App\Service\Mailer\Mailer;
-use App\Service\Mailer\Model\NewStructureTrialRequestValidationModel;
+use App\Service\Mailer\Model\NewStructureArtistPremiumTrialRequestValidationModel;
+use App\Service\Mailer\Model\SubdomainChangeModel;
 use App\Service\Organization\OrganizationFactory;
 use App\Service\Utils\UrlBuilder;
+use Doctrine\DBAL\Exception;
 use Doctrine\ORM\EntityManagerInterface;
+use JsonException;
+use Psr\Log\LoggerInterface;
 use RuntimeException;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
 use Symfony\Component\Messenger\Exception\ExceptionInterface;
 use Symfony\Component\Messenger\MessageBusInterface;
+use Symfony\Component\Serializer\SerializerInterface;
 use Symfony\Component\Uid\Uuid;
 
 /**
@@ -30,12 +41,15 @@ use Symfony\Component\Uid\Uuid;
 readonly class ShopService
 {
     public function __construct(
-        private EntityManagerInterface  $entityManager,
-        private Mailer                  $mailer,
-        private string                  $baseUrl,
-        private MessageBusInterface     $messageBus,
-        private ApiLegacyRequestService $apiLegacyRequestService,
-        private OrganizationFactory     $organizationFactory,
+        private EntityManagerInterface $entityManager,
+        private Mailer                 $mailer,
+        private string                 $publicBaseUrl,
+        private OrganizationFactory    $organizationFactory,
+        private SerializerInterface    $serializer,
+        private LoggerInterface        $logger,
+        private DolibarrApiService     $dolibarrApiService,
+        private DolibarrUtils          $dolibarrUtils,
+        private MessageBusInterface    $messageBus,
     ) {
     }
 
@@ -91,18 +105,20 @@ readonly class ShopService
      */
     public function processShopRequest(ShopRequest $shopRequest): void
     {
+        // Dispatch appropriate job based on request type
+        switch ($shopRequest->getType()->value) {
+            case ShopRequestType::NEW_STRUCTURE_ARTIST_PREMIUM_TRIAL->value:
+                $this->messageBus->dispatch(
+                    new NewStructureArtistPremiumTrial($shopRequest->getToken())
+                );
+                break;
+            default:
+                throw new RuntimeException('request type not supported');
+        }
+
         $shopRequest->setStatus(ShopRequestStatus::VALIDATED);
         $this->entityManager->persist($shopRequest);
         $this->entityManager->flush();
-
-        // Dispatch appropriate job based on request type
-        if ($shopRequest->getType() === ShopRequestType::NEW_STRUCTURE_ARTIST_PREMIUM_TRIAL) {
-            $this->messageBus->dispatch(
-                new NewStructureArtistPremiumTrial($shopRequest->getToken())
-            );
-        } else {
-            throw new RuntimeException('Unknown request type');
-        }
     }
 
     /**
@@ -132,14 +148,16 @@ readonly class ShopService
     protected function sendRequestValidationLink(ShopRequest $shopRequest): void
     {
         $validationUrl = UrlBuilder::concat(
-            $this->baseUrl,
-            ['shop/validate', $shopRequest->getToken()]
+            $this->publicBaseUrl,
+            ['/api/public/shop/validate', $shopRequest->getToken()]
         );
 
         $data = $shopRequest->getData();
 
-        $model = new NewStructureTrialRequestValidationModel();
-        $model->setToken($shopRequest->getToken())
+        $model = new NewStructureArtistPremiumTrialRequestValidationModel();
+        $model
+            ->setSenderId(AccessIdsEnum::ADMIN_2IOPENSERVICE->value)
+            ->setToken($shopRequest->getToken())
             ->setRepresentativeEmail($data['representativeEmail'] ?? '')
             ->setRepresentativeFirstName($data['representativeFirstName'] ?? '')
             ->setRepresentativeLastName($data['representativeLastName'] ?? '')
@@ -159,44 +177,133 @@ readonly class ShopService
      * @param Organization $organization The organization to start the trial for
      * @param NewStructureArtistPremiumTrialRequest $request The trial request data
      *
-     * @return bool True if the trial was started successfully, false otherwise
-     *
-     * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
+     * @throws Exception
+     * @throws JsonException
      */
-    public function startArtistPremiumTrial(Organization $organization, NewStructureArtistPremiumTrialRequest $request): bool
+    public function startArtistPremiumTrial(Organization $organization, NewStructureArtistPremiumTrialRequest $request): void
     {
-        // Prepare the request data
-        $data = [
-            "organization" => [
-                "streetAddress" => $request->getAddress(),
-                "streetAddressSecond" => $request->getAddressComplement(),
-                "streetAddressThird" => "",
-                "cp" => $request->getPostalCode(),
-                "city" => $request->getCity(),
-                "name" => $organization->getName(),
-                "organizationAddressPostalId" => null
-            ],
-            "access" => [
-                "isAdmin" => true,
-                "email" => $request->getRepresentativeEmail(),
-                "contactPointId" => null,
-                "name" => $request->getRepresentativeLastName(),
-                "givenName" => $request->getRepresentativeFirstName(),
-                "function" => $request->getRepresentativeFunction(),
-                "telphone" => $request->getRepresentativePhone()
-            ]
-        ];
-
-        try {
-            $response = $this->apiLegacyRequestService->request(
-                'POST',
-                '/api/trial/artist_premium',
-                $data
-            );
-
-            return $response->getStatusCode() === Response::HTTP_OK;
-        } catch (\Exception $e) {
-            return false;
+        // Update settings
+        $settings = $organization->getSettings();
+        $settings->setProductBeforeTrial( $organization->getSettings()->getProduct());
+        $settings->setTrialActive(true);
+        $settings->setLastTrialStartDate(new \DateTime('now'));
+        $settings->setProduct(SettingsProductEnum::ARTIST_PREMIUM);
+        $this->entityManager->persist($settings);
+        $this->entityManager->flush();
+
+        $dolibarrSocietyId = $this->dolibarrApiService->getSocietyId($organization->getId());
+
+        // Create contract in dolibarr
+        $dolibarrProductId = $this->dolibarrUtils->getProductId(
+            SettingsProductEnum::ARTIST_PREMIUM, true
+        );
+
+        $contractId = $this->dolibarrApiService->createContract(
+            $dolibarrSocietyId, $dolibarrProductId, true, 1
+        );
+
+        $this->dolibarrApiService->createContractLine($contractId, $dolibarrProductId, 1);
+
+        // Maj le représentant commercial dans dolibarr
+        $this->dolibarrUtils->updateSocietyCommercialsWithApi($dolibarrSocietyId);
+
+        // Met à jour le produit dans Dolibarr
+        $productName = $this->dolibarrUtils->getDolibarrProductName(SettingsProductEnum::ARTIST_PREMIUM, true);
+        $this->dolibarrApiService->updateSocietyProduct($dolibarrSocietyId, $productName);
+
+        // Ajoute une entrée aux actions commerciales dolibarr
+        $message = sprintf(
+            "Action réalisé par : %s %s.<br>Fonction : %s<br>Mail:%s<br>Tel:%s",
+            $request->getRepresentativeFirstName(),
+            $request->getRepresentativeLastName(),
+            $request->getRepresentativeFunction(),
+            $request->getRepresentativeEmail(),
+            $request->getRepresentativePhone()
+        );
+
+        $this->dolibarrUtils->addActionComm(
+            $dolibarrSocietyId, "Ouverture de la période d’essai", $message
+        );
+    }
+
+    protected function handleNewStructureArtistPremiumTrialRequest(string $token): void
+    {
+        // Retrieve the ShopRequest entity using its token
+        $shopRequest = $this->entityManager->find(ShopRequest::class, $token);
+
+        if (!$shopRequest) {
+            $this->logger->error('Cannot find ShopRequest with token: ' . $token);
+            return;
         }
+
+        // Convert the stored JSON data to a NewStructureArtistPremiumTrialRequest object
+        $data = $shopRequest->getData();
+        $trialRequest = $this->serializer->deserialize(
+            json_encode($data),
+            NewStructureArtistPremiumTrialRequest::class,
+            'json'
+        );
+
+        $organization = $this->createOrganization($trialRequest);
+
+        // Start the artist premium trial
+        $this->startArtistPremiumTrial($organization, $trialRequest);
+
+        $this->logger->info('Successfully processed NewStructureArtistPremiumTrial for token: ' . $token);
+    }
+
+    protected function createOrganization(NewStructureArtistPremiumTrialRequest $trialRequest): Organization
+    {
+        // Generate an OrganizationCreationRequest object
+        $organizationCreationRequest = new OrganizationCreationRequest();
+        $organizationCreationRequest->setName($trialRequest->getStructureName());
+        $organizationCreationRequest->setStreetAddress1($trialRequest->getAddress());
+        $organizationCreationRequest->setStreetAddress2($trialRequest->getAddressComplement());
+        $organizationCreationRequest->setPostalCode($trialRequest->getPostalCode());
+        $organizationCreationRequest->setCity($trialRequest->getCity());
+        $organizationCreationRequest->setEmail($trialRequest->getStructureEmail());
+        $organizationCreationRequest->setPrincipalType($trialRequest->getStructureType());
+        $organizationCreationRequest->setLegalStatus($trialRequest->getLegalStatus());
+        $organizationCreationRequest->setSiretNumber($trialRequest->getSiren());
+
+        // TODO: à améliorer
+        $organizationCreationRequest->setPhoneNumber(
+            '+33' . substr($trialRequest->getRepresentativePhone(), 1)
+        );
+
+        // Generate a subdomain from the structure name
+        // TODO: à améliorer
+        $subdomain = $this->generateSubdomain($trialRequest->getStructureName());
+        $organizationCreationRequest->setSubdomain($subdomain);
+
+        // Set default values
+        $organizationCreationRequest->setProduct(SettingsProductEnum::FREEMIUM);
+        $organizationCreationRequest->setCreateWebsite(false);
+        $organizationCreationRequest->setClient(false);
+        $organizationCreationRequest->setCreationDate(new \DateTime());
+
+        // Create the organization
+        return $this->organizationFactory->create($organizationCreationRequest);
+    }
+
+    /**
+     * Generate a subdomain from a structure name.
+     */
+    private function generateSubdomain(string $name): string
+    {
+        // Remove accents and special characters
+        $subdomain = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
+        // Replace spaces and special characters with hyphens
+        $subdomain = preg_replace('/[^a-zA-Z0-9]/', '-', $subdomain);
+        // Convert to lowercase
+        $subdomain = strtolower($subdomain);
+        // Remove consecutive hyphens
+        $subdomain = preg_replace('/-+/', '-', $subdomain);
+        // Trim hyphens from beginning and end
+        $subdomain = trim($subdomain, '-');
+        // Limit length
+        $subdomain = substr($subdomain, 0, 30);
+
+        return $subdomain;
     }
 }

+ 1 - 0
src/State/Provider/Shop/ShopRequestProvider.php

@@ -61,6 +61,7 @@ final class ShopRequestProvider implements ProviderInterface
         $this->shopService->processShopRequest($shopRequest);
 
         // Return a success response
+        // TODO: redirect to a confirmation page
         return new Response('Request validated successfully', Response::HTTP_OK);
     }
 

+ 2 - 2
templates/emails/base.html.twig

@@ -33,7 +33,7 @@
             <row>
                 <columns small="12" class="header">
                     <spacer size="10"></spacer>
-                    <p class="white">{{ organization.name }}</p>
+                    <p class="white">{{ organization.name ?? '' }}</p>
                 </columns>
             </row>
         {% endblock %}
@@ -96,4 +96,4 @@
 
     </container>
 
-{% endapply %}
+{% endapply %}

+ 0 - 0
templates/emails/shop/new-structure-validation.html.twig → templates/emails/shop/new-structure-artist-premium-trial-validation.html.twig