Browse Source

various fixes and add unit tests

Olivier Massot 5 months ago
parent
commit
8ebb639d25

+ 55 - 22
src/Service/Shop/ShopService.php

@@ -22,10 +22,12 @@ use App\Service\Mailer\Mailer;
 use App\Service\Mailer\Model\NewStructureArtistPremiumTrialRequestValidationModel;
 use App\Service\Mailer\Model\SubdomainChangeModel;
 use App\Service\Organization\OrganizationFactory;
+use App\Service\Utils\DatesUtils;
 use App\Service\Utils\UrlBuilder;
 use Doctrine\DBAL\Exception;
 use Doctrine\ORM\EntityManagerInterface;
 use JsonException;
+use libphonenumber\PhoneNumberUtil;
 use Psr\Log\LoggerInterface;
 use RuntimeException;
 use Symfony\Component\HttpFoundation\Response;
@@ -38,8 +40,10 @@ use Symfony\Component\Uid\Uuid;
 /**
  * Service for managing shop requests.
  */
-readonly class ShopService
+class ShopService
 {
+    protected PhoneNumberUtil $phoneNumberUtil;
+
     public function __construct(
         private EntityManagerInterface $entityManager,
         private Mailer                 $mailer,
@@ -51,6 +55,7 @@ readonly class ShopService
         private DolibarrUtils          $dolibarrUtils,
         private MessageBusInterface    $messageBus,
     ) {
+        $this->phoneNumberUtil = PhoneNumberUtil::getInstance();
     }
 
     /**
@@ -82,17 +87,9 @@ readonly class ShopService
     protected function controlShopRequestData(ShopRequestType $type, array $data): void
     {
         if ($type === ShopRequestType::NEW_STRUCTURE_ARTIST_PREMIUM_TRIAL) {
-            // Create a minimal OrganizationCreationRequest with required info
-            $organizationCreationRequest = new OrganizationCreationRequest();
-            $organizationCreationRequest->setName($data['structureName'] ?? '');
-            $organizationCreationRequest->setCity($data['city'] ?? '');
-            $organizationCreationRequest->setPostalCode($data['postalCode'] ?? '');
-            $organizationCreationRequest->setStreetAddress1($data['address'] ?? '');
-            $organizationCreationRequest->setStreetAddress2($data['addressComplement'] ?? '');
-            $organizationCreationRequest->setStreetAddress3('');
-
-            // Check if organization already exists
-            $this->organizationFactory->interruptIfOrganizationExists($organizationCreationRequest);
+            /** @var NewStructureArtistPremiumTrialRequest $request */
+            $request = $data;
+            $this->validateNewStructureArtistPremiumTrialRequest($request);
         } else {
             throw new RuntimeException('request type not supported');
         }
@@ -180,13 +177,13 @@ readonly class ShopService
      * @throws Exception
      * @throws JsonException
      */
-    public function startArtistPremiumTrial(Organization $organization, NewStructureArtistPremiumTrialRequest $request): void
+    protected function startArtistPremiumTrial(Organization $organization, NewStructureArtistPremiumTrialRequest $request): void
     {
         // Update settings
         $settings = $organization->getSettings();
         $settings->setProductBeforeTrial( $organization->getSettings()->getProduct());
         $settings->setTrialActive(true);
-        $settings->setLastTrialStartDate(new \DateTime('now'));
+        $settings->setLastTrialStartDate(DatesUtils::new());
         $settings->setProduct(SettingsProductEnum::ARTIST_PREMIUM);
         $this->entityManager->persist($settings);
         $this->entityManager->flush();
@@ -222,7 +219,7 @@ readonly class ShopService
         );
 
         $this->dolibarrUtils->addActionComm(
-            $dolibarrSocietyId, "Ouverture de la période d’essai", $message
+            $dolibarrSocietyId, 'Ouverture de la période d\'essai', $message
         );
     }
 
@@ -265,11 +262,7 @@ readonly class ShopService
         $organizationCreationRequest->setPrincipalType($trialRequest->getStructureType());
         $organizationCreationRequest->setLegalStatus($trialRequest->getLegalStatus());
         $organizationCreationRequest->setSiretNumber($trialRequest->getSiren());
-
-        // TODO: à améliorer
-        $organizationCreationRequest->setPhoneNumber(
-            '+33' . substr($trialRequest->getRepresentativePhone(), 1)
-        );
+        $organizationCreationRequest->setPhoneNumber($trialRequest->getRepresentativePhone());
 
         // Generate a subdomain from the structure name
         // TODO: à améliorer
@@ -289,10 +282,23 @@ readonly class ShopService
     /**
      * Generate a subdomain from a structure name.
      */
-    private function generateSubdomain(string $name): string
+    protected function generateSubdomain(string $name): string
     {
+        // Special case for École to ensure it becomes ecole
+        $name = str_replace(['É', 'é'], 'e', $name);
+
         // Remove accents and special characters
-        $subdomain = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
+        if (function_exists('transliterator_transliterate')) {
+            $subdomain = transliterator_transliterate('Any-Latin; Latin-ASCII; [^a-zA-Z0-9] > \'-\'; Lower()', $name);
+            if ($subdomain === false) {
+                // Fallback if transliteration fails
+                $subdomain = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
+            }
+        } else {
+            // Fallback if transliterator is not available
+            $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
@@ -304,6 +310,33 @@ readonly class ShopService
         // Limit length
         $subdomain = substr($subdomain, 0, 30);
 
+        // Ensure no trailing hyphens after truncation
+        $subdomain = rtrim($subdomain, '-');
+
         return $subdomain;
     }
+
+    /**
+     * @param NewStructureArtistPremiumTrialRequest $request
+     * @return void
+     */
+    protected function validateNewStructureArtistPremiumTrialRequest(
+        NewStructureArtistPremiumTrialRequest $request
+    ): void
+    {
+        // Validate phone number
+        if (!$this->phoneNumberUtil->isPossibleNumber($request->getRepresentativePhone())) {
+            throw new RuntimeException('Invalid phone number');
+        }
+
+        // Check if organization already exists
+        $organizationCreationRequest = new OrganizationCreationRequest();
+        $organizationCreationRequest->setName($request->getStructureName() ?? '');
+        $organizationCreationRequest->setCity($request->getCity() ?? '');
+        $organizationCreationRequest->setPostalCode($request->getPostalCode() ?? '');
+        $organizationCreationRequest->setStreetAddress1($request->getAddress() ?? '');
+        $organizationCreationRequest->setStreetAddress2($request->getAddressComplement() ?? '');
+        $organizationCreationRequest->setStreetAddress3('');
+        $this->organizationFactory->interruptIfOrganizationExists($organizationCreationRequest);
+    }
 }

+ 592 - 1
tests/Unit/Service/Dolibarr/DolibarrApiServiceTest.php

@@ -4,6 +4,7 @@ namespace App\Tests\Unit\Service\Dolibarr;
 
 use App\Entity\Organization\Organization;
 use App\Service\Dolibarr\DolibarrApiService;
+use App\Service\Utils\DatesUtils;
 use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -465,7 +466,7 @@ class DolibarrApiServiceTest extends TestCase
         ];
 
         $response = $this->getMockBuilder(ResponseInterface::class)->getMock();
-        $response->method('getContent')->willReturn('456');
+        $response->method('getContent')->willReturn(json_encode(456));
 
         $dolibarrApiService
             ->expects(self::once())
@@ -478,6 +479,52 @@ class DolibarrApiServiceTest extends TestCase
         $this->assertEquals(456, $result);
     }
 
+    /**
+     * @see DolibarrApiService::getSocietyId()
+     */
+    public function testGetSocietyId(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['getSocietyId'])
+            ->getMock();
+
+        $organizationId = 123;
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('getSociety')
+            ->with($organizationId)
+            ->willReturn(['id' => '456']);
+
+        $societyId = $dolibarrApiService->getSocietyId($organizationId);
+
+        $this->assertEquals(456, $societyId);
+    }
+
+    /**
+     * @see DolibarrApiService::getSocietyId()
+     */
+    public function testGetSocietyIdNotExisting(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['getSocietyId'])
+            ->getMock();
+
+        $organizationId = 123;
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('getSociety')
+            ->with($organizationId)
+            ->willReturn(null);
+
+        $societyId = $dolibarrApiService->getSocietyId($organizationId);
+
+        $this->assertNull($societyId);
+    }
+
     /**
      * @see DolibarrApiService::createSociety
      */
@@ -514,6 +561,130 @@ class DolibarrApiServiceTest extends TestCase
         $this->assertEquals(456, $result);
     }
 
+    /**
+     * @see DolibarrApiService::getLastOrder()
+     */
+    public function testGetLastOrder(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['getLastOrder'])
+            ->getMock();
+
+        $socId = 1;
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('getJsonContent')
+            ->with(
+                'orders',
+                [
+                    'sortfield' => 't.date_valid',
+                    'sortorder' => 'DESC',
+                    'limit' => 1,
+                    'sqlfilters' => 'fk_soc:=:'.$socId,
+                ]
+            )
+            ->willReturn([['id' => 10, 'ref' => 'ORDER123']]);
+
+        $result = $dolibarrApiService->getLastOrder($socId);
+
+        $this->assertEquals(['id' => 10, 'ref' => 'ORDER123'], $result);
+    }
+
+    /**
+     * @see DolibarrApiService::getLastOrder()
+     */
+    public function testGetLastOrderEmpty(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['getLastOrder'])
+            ->getMock();
+
+        $socId = 1;
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('getJsonContent')
+            ->with(
+                'orders',
+                [
+                    'sortfield' => 't.date_valid',
+                    'sortorder' => 'DESC',
+                    'limit' => 1,
+                    'sqlfilters' => 'fk_soc:=:'.$socId,
+                ]
+            )
+            ->willReturn([]);
+
+        $result = $dolibarrApiService->getLastOrder($socId);
+
+        $this->assertNull($result);
+    }
+
+    /**
+     * @see DolibarrApiService::getLastOrder()
+     */
+    public function testGetLastOrderNotFound(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['getLastOrder'])
+            ->getMock();
+
+        $socId = 1;
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('getJsonContent')
+            ->with(
+                'orders',
+                [
+                    'sortfield' => 't.date_valid',
+                    'sortorder' => 'DESC',
+                    'limit' => 1,
+                    'sqlfilters' => 'fk_soc:=:'.$socId,
+                ]
+            )
+            ->willThrowException(new HttpException(404));
+
+        $result = $dolibarrApiService->getLastOrder($socId);
+
+        $this->assertNull($result);
+    }
+
+    /**
+     * @see DolibarrApiService::getLastOrder()
+     */
+    public function testGetLastOrderError(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['getLastOrder'])
+            ->getMock();
+
+        $socId = 1;
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('getJsonContent')
+            ->with(
+                'orders',
+                [
+                    'sortfield' => 't.date_valid',
+                    'sortorder' => 'DESC',
+                    'limit' => 1,
+                    'sqlfilters' => 'fk_soc:=:'.$socId,
+                ]
+            )
+            ->willThrowException(new HttpException(500));
+
+        $this->expectException(HttpException::class);
+
+        $dolibarrApiService->getLastOrder($socId);
+    }
+
     public function testSwitchSocietyToProspect(): void
     {
         $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
@@ -538,6 +709,79 @@ class DolibarrApiServiceTest extends TestCase
         $dolibarrApiService->switchSocietyToProspect(123);
     }
 
+    /**
+     * @see DolibarrApiService::downloadBillingDocPdf()
+     */
+    public function testDownloadBillingDocPdf(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['downloadBillingDocPdf'])
+            ->getMock();
+
+        $type = 'invoice';
+        $docRef = 'FA2023-0001';
+        $expectedResult = [
+            'filename' => 'FA2023-0001.pdf',
+            'content-type' => 'application/pdf',
+            'filesize' => 10660,
+            'content' => 'base64encodedcontent',
+            'encoding' => 'base64'
+        ];
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('getJsonContent')
+            ->with('documents/download?modulepart=invoice&original_file='.urlencode("$docRef/$docRef.pdf"))
+            ->willReturn($expectedResult);
+
+        $result = $dolibarrApiService->downloadBillingDocPdf($type, $docRef);
+
+        $this->assertEquals($expectedResult, $result);
+    }
+
+    /**
+     * @see DolibarrApiService::downloadBillingDocPdf()
+     */
+    public function testDownloadBillingDocPdfInvalidType(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['downloadBillingDocPdf'])
+            ->getMock();
+
+        $type = 'invalid_type';
+        $docRef = 'FA2023-0001';
+
+        $this->expectException(\InvalidArgumentException::class);
+
+        $dolibarrApiService->downloadBillingDocPdf($type, $docRef);
+    }
+
+    /**
+     * @see DolibarrApiService::downloadBillingDocPdf()
+     */
+    public function testDownloadBillingDocPdfError(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['downloadBillingDocPdf'])
+            ->getMock();
+
+        $type = 'invoice';
+        $docRef = 'FA2023-0001';
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('getJsonContent')
+            ->with('documents/download?modulepart=invoice&original_file='.urlencode("$docRef/$docRef.pdf"))
+            ->willThrowException(new HttpException(500));
+
+        $this->expectException(HttpException::class);
+
+        $dolibarrApiService->downloadBillingDocPdf($type, $docRef);
+    }
+
     public function testSwitchSocietyToProspectWithError(): void
     {
         $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
@@ -564,4 +808,351 @@ class DolibarrApiServiceTest extends TestCase
 
         $dolibarrApiService->switchSocietyToProspect(123);
     }
+    /**
+     * @see DolibarrApiService::createContract()
+     */
+    public function testCreateContract(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['createContract'])
+            ->getMock();
+
+        // Mock DatesUtils to return a fixed date
+        DatesUtils::setFakeDatetime('2023-01-01 12:00:00');
+        $date = DatesUtils::new();
+
+        $socId = 1;
+        $productId = 2;
+        $isNewClient = false;
+        $duration = 12;
+
+        // Mock product data
+        $productData = [
+            'label' => 'Test Product',
+            'description' => 'Test Description',
+            'price' => '100.00',
+            'price_base_type' => 'HT',
+            'tva_tx' => '20.00',
+        ];
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('getJsonContent')
+            ->with("products/$productId")
+            ->willReturn($productData);
+
+        // Expected contract data
+        $expectedBody = [
+            "socid" => $socId,
+            "date_contrat" => $date->format('Y-m-d'),
+            "commercial_signature_id" => 8,
+            "commercial_suivi_id" => 8,
+            'statut' => 1,
+            'lines' => [
+                [
+                    'fk_product' => $productId,
+                    'label' => $productData['label'],
+                    'desc' => $productData['description'],
+                    'qty' => 1,
+                    'subprice' => number_format((float)$productData['price'], 2),
+                    'price_base_type' => $productData['price_base_type'],
+                    'tva_tx' => $productData['tva_tx'],
+                ]
+            ],
+            'array_options' => [
+                'options_ec_amount' => number_format((float)$productData['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,
+                'options_ec_payment_mode' => 6,
+                'options_ec_account' => 1,
+                'options_logicielfact' => 1,
+                'options_versionfact' => 2,
+                'options_2iopen_origvente' => 3, // Evolution (3) for existing client
+            ]
+        ];
+
+        $response = $this->getMockBuilder(ResponseInterface::class)->getMock();
+        $response->method('getContent')->willReturn('123');
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('post')
+            ->with('contracts', $expectedBody)
+            ->willReturn($response);
+
+        $result = $dolibarrApiService->createContract($socId, $productId, $isNewClient, $duration);
+
+        $this->assertEquals(123, $result);
+    }
+
+    /**
+     * @see DolibarrApiService::createContract()
+     */
+    public function testCreateContractForNewClient(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['createContract'])
+            ->getMock();
+
+        // Mock DatesUtils to return a fixed date
+        DatesUtils::setFakeDatetime('2023-01-01 12:00:00');
+        $date = DatesUtils::new();
+
+        $socId = 1;
+        $productId = 2;
+        $isNewClient = true; // Testing for new client
+        $duration = 12;
+
+        // Mock product data
+        $productData = [
+            'label' => 'Test Product',
+            'description' => 'Test Description',
+            'price' => '100.00',
+            'price_base_type' => 'HT',
+            'tva_tx' => '20.00',
+        ];
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('getJsonContent')
+            ->with("products/$productId")
+            ->willReturn($productData);
+
+        // Expected contract data with originVente = 1 for new client
+        $expectedBody = [
+            "socid" => $socId,
+            "date_contrat" => $date->format('Y-m-d'),
+            "commercial_signature_id" => 8,
+            "commercial_suivi_id" => 8,
+            'statut' => 1,
+            'lines' => [
+                [
+                    'fk_product' => $productId,
+                    'label' => $productData['label'],
+                    'desc' => $productData['description'],
+                    'qty' => 1,
+                    'subprice' => number_format((float)$productData['price'], 2),
+                    'price_base_type' => $productData['price_base_type'],
+                    'tva_tx' => $productData['tva_tx'],
+                ]
+            ],
+            'array_options' => [
+                'options_ec_amount' => number_format((float)$productData['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,
+                'options_ec_payment_mode' => 6,
+                'options_ec_account' => 1,
+                'options_logicielfact' => 1,
+                'options_versionfact' => 2,
+                'options_2iopen_origvente' => 1, // New client (1)
+            ]
+        ];
+
+        $response = $this->getMockBuilder(ResponseInterface::class)->getMock();
+        $response->method('getContent')->willReturn('123');
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('post')
+            ->with('contracts', $expectedBody)
+            ->willReturn($response);
+
+        $result = $dolibarrApiService->createContract($socId, $productId, $isNewClient, $duration);
+
+        $this->assertEquals(123, $result);
+    }
+
+    /**
+     * @see DolibarrApiService::createContractLine()
+     */
+    public function testCreateContractLine(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['createContractLine'])
+            ->getMock();
+
+        // Mock DatesUtils to return a fixed date
+        DatesUtils::setFakeDatetime('2023-01-01 12:00:00');
+        $date = DatesUtils::new();
+        $endDate = DatesUtils::new()->modify('+12 months')->modify('-1 day');
+
+        $contractId = 1;
+        $productId = 2;
+        $duration = 12;
+
+        // Mock product data
+        $productData = [
+            'label' => 'Test Product',
+            'description' => 'Test Description',
+            'price' => '100.00',
+            'price_base_type' => 'HT',
+            'tva_tx' => '20.00',
+        ];
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('getJsonContent')
+            ->with("products/$productId")
+            ->willReturn($productData);
+
+        // Expected contract line data
+        $expectedBody = [
+            'fk_product' => $productId,
+            'label' => $productData['label'],
+            'desc' => $productData['description'],
+            'qty' => 1,
+            'subprice' => number_format((float)$productData['price'], 2),
+            'price_base_type' => $productData['price_base_type'],
+            'tva_tx' => $productData['tva_tx'],
+            'date_start' => $date->format('Y-m-d'),
+            'date_end' => $endDate->format('Y-m-d'),
+        ];
+
+        $response = $this->getMockBuilder(ResponseInterface::class)->getMock();
+        $response->method('getContent')->willReturn('456');
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('post')
+            ->with("contracts/$contractId/lines", $expectedBody)
+            ->willReturn($response);
+
+        $result = $dolibarrApiService->createContractLine($contractId, $productId, $duration);
+
+        $this->assertEquals(456, $result);
+    }
+
+    /**
+     * @see DolibarrApiService::updateSocietyProduct()
+     */
+    public function testUpdateSocietyProduct(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['updateSocietyProduct'])
+            ->getMock();
+
+        $socId = 1;
+        $productName = 'ARTIST_PREMIUM';
+
+        $expectedBody = [
+            'array_options' => [
+                'options_2iopen_software_opentalent' => $productName
+            ]
+        ];
+
+        $response = $this->getMockBuilder(ResponseInterface::class)->getMock();
+
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('put')
+            ->with("thirdparties/$socId", $expectedBody)
+            ->willReturn($response);
+
+        $dolibarrApiService->updateSocietyProduct($socId, $productName);
+    }
+
+    /**
+     * @see DolibarrApiService::createContract()
+     */
+    public function testCreateContractError(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['createContract'])
+            ->getMock();
+
+        $socId = 1;
+        $productId = 2;
+        $isNewClient = false;
+        $duration = 12;
+
+        // Mock product data retrieval error
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('getJsonContent')
+            ->with("products/$productId")
+            ->willThrowException(new HttpException(500, 'Product not found'));
+
+        $this->expectException(HttpException::class);
+        $this->expectExceptionMessage('Product not found');
+
+        $dolibarrApiService->createContract($socId, $productId, $isNewClient, $duration);
+    }
+
+    /**
+     * @see DolibarrApiService::createContractLine()
+     */
+    public function testCreateContractLineError(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['createContractLine'])
+            ->getMock();
+
+        $contractId = 1;
+        $productId = 2;
+        $duration = 12;
+
+        // Mock product data retrieval error
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('getJsonContent')
+            ->with("products/$productId")
+            ->willThrowException(new HttpException(500, 'Product not found'));
+
+        $this->expectException(HttpException::class);
+        $this->expectExceptionMessage('Product not found');
+
+        $dolibarrApiService->createContractLine($contractId, $productId, $duration);
+    }
+
+    /**
+     * @see DolibarrApiService::updateSocietyProduct()
+     */
+    public function testUpdateSocietyProductError(): void
+    {
+        $dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)
+            ->setConstructorArgs([$this->client])
+            ->setMethodsExcept(['updateSocietyProduct'])
+            ->getMock();
+
+        $socId = 1;
+        $productName = 'ARTIST_PREMIUM';
+
+        $expectedBody = [
+            'array_options' => [
+                'options_2iopen_software_opentalent' => $productName
+            ]
+        ];
+
+        // Mock put method to throw an exception
+        $dolibarrApiService
+            ->expects(self::once())
+            ->method('put')
+            ->with("thirdparties/$socId", $expectedBody)
+            ->willThrowException(new HttpException(500, 'Error updating society product'));
+
+        $this->expectException(HttpException::class);
+        $this->expectExceptionMessage('Error updating society product');
+
+        $dolibarrApiService->updateSocietyProduct($socId, $productName);
+    }
 }

+ 242 - 0
tests/Unit/Service/Dolibarr/DolibarrUtilsTest.php

@@ -0,0 +1,242 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Tests\Unit\Service\Dolibarr;
+
+use App\Enum\Organization\SettingsProductEnum;
+use App\Service\Dolibarr\DolibarrUtils;
+use App\Service\Utils\DatesUtils;
+use Doctrine\DBAL\Connection;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Class to expose protected methods for testing
+ */
+class TestableDolibarrUtils extends DolibarrUtils
+{
+    public function executeQueryPublic(string $sql): void
+    {
+        $this->executeQuery($sql);
+    }
+}
+
+class DolibarrUtilsTest extends TestCase
+{
+    private Connection|MockObject $dolibarrConnection;
+    private TestableDolibarrUtils $dolibarrUtils;
+
+    protected function setUp(): void
+    {
+        $this->dolibarrConnection = $this->createMock(Connection::class);
+        $this->dolibarrUtils = new TestableDolibarrUtils($this->dolibarrConnection);
+    }
+
+    /**
+     * @see DolibarrUtils::getProductId()
+     */
+    public function testGetProductIdForArtistPremiumTrial(): void
+    {
+        $result = $this->dolibarrUtils->getProductId(
+            SettingsProductEnum::ARTIST_PREMIUM,
+            true,
+            false
+        );
+
+        $this->assertEquals(DolibarrUtils::ARTIST_PREMIUM_TRIAL_PRODUCT_ID, $result);
+    }
+
+    /**
+     * @see DolibarrUtils::getProductId()
+     */
+    public function testGetProductIdForArtistPremiumCmf(): void
+    {
+        $result = $this->dolibarrUtils->getProductId(
+            SettingsProductEnum::ARTIST_PREMIUM,
+            false,
+            true
+        );
+
+        $this->assertEquals(DolibarrUtils::ARTIST_PREMIUM_CMF_PRODUCT_ID, $result);
+    }
+
+    /**
+     * @see DolibarrUtils::getProductId()
+     */
+    public function testGetProductIdForArtistPremium(): void
+    {
+        $result = $this->dolibarrUtils->getProductId(
+            SettingsProductEnum::ARTIST_PREMIUM,
+            false,
+            false
+        );
+
+        $this->assertEquals(DolibarrUtils::ARTIST_PREMIUM_PRODUCT_ID, $result);
+    }
+
+    /**
+     * @see DolibarrUtils::getProductId()
+     */
+    public function testGetProductIdForArtistCmf(): void
+    {
+        $result = $this->dolibarrUtils->getProductId(
+            SettingsProductEnum::ARTIST,
+            false,
+            true
+        );
+
+        $this->assertEquals(DolibarrUtils::ARTIST_STANDARD_CMF_PRODUCT_ID, $result);
+    }
+
+    /**
+     * @see DolibarrUtils::getProductId()
+     */
+    public function testGetProductIdWithInvalidContractType(): void
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage("Invalid contract type");
+
+        $this->dolibarrUtils->getProductId(
+            SettingsProductEnum::SCHOOL,
+            false,
+            false
+        );
+    }
+
+    /**
+     * @see DolibarrUtils::executeQuery()
+     */
+    public function testExecuteQuery(): void
+    {
+        $sql = "DELETE FROM llx_societe_commerciaux WHERE fk_soc = 123";
+
+        $this->dolibarrConnection
+            ->expects($this->once())
+            ->method('executeQuery')
+            ->with($sql);
+
+        $this->dolibarrUtils->executeQueryPublic($sql);
+    }
+
+    /**
+     * @see DolibarrUtils::updateSocietyCommercialsWithApi()
+     */
+    public function testUpdateSocietyCommercialsWithApi(): void
+    {
+        $societyId = 123;
+        $apiUserId = 8;
+
+        $this->dolibarrConnection
+            ->expects($this->exactly(2))
+            ->method('executeQuery')
+            ->withConsecutive(
+                ["DELETE FROM llx_societe_commerciaux WHERE fk_soc = $societyId"],
+                ["INSERT INTO llx_societe_commerciaux (fk_soc, fk_user) 
+                   VALUES ($societyId, $apiUserId)"]
+            );
+
+        $this->dolibarrUtils->updateSocietyCommercialsWithApi($societyId);
+    }
+
+    /**
+     * @see DolibarrUtils::addActionComm()
+     */
+    public function testAddActionComm(): void
+    {
+        $societyId = 123;
+        $title = "Test Title";
+        $message = "Test Message";
+        $apiUserId = 8;
+
+        // Mock DatesUtils to return a fixed date
+        $tz = new \DateTimeZone('Europe/Paris');
+        $mockDate = new \DateTime('2023-01-01 12:00:00', $tz);
+        $formattedDate = $mockDate->format('Y-m-d H:i:s');
+
+        // Create a partial mock of DatesUtils to control the 'new' static method
+        $datesMock = $this->createMock(DatesUtils::class);
+        DatesUtils::setFakeDatetime('2023-01-01 12:00:00');
+
+        $this->dolibarrConnection
+            ->expects($this->once())
+            ->method('executeQuery')
+            ->with(
+                "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', '$formattedDate', '$formattedDate', '$formattedDate', $apiUserId, $apiUserId, $apiUserId, -1)"
+            );
+
+        $this->dolibarrUtils->addActionComm($societyId, $title, $message);
+    }
+
+    /**
+     * @see DolibarrUtils::getDolibarrProductName()
+     */
+    public function testGetDolibarrProductNameForArtist(): void
+    {
+        $result = $this->dolibarrUtils->getDolibarrProductName(SettingsProductEnum::ARTIST);
+        $this->assertEquals("Opentalent Artist", $result);
+    }
+
+    /**
+     * @see DolibarrUtils::getDolibarrProductName()
+     */
+    public function testGetDolibarrProductNameForArtistPremiumTrial(): void
+    {
+        $result = $this->dolibarrUtils->getDolibarrProductName(SettingsProductEnum::ARTIST_PREMIUM, true);
+        $this->assertEquals("Opentalent Artist Premium (Essai)", $result);
+    }
+
+    /**
+     * @see DolibarrUtils::getDolibarrProductName()
+     */
+    public function testGetDolibarrProductNameForArtistPremium(): void
+    {
+        $result = $this->dolibarrUtils->getDolibarrProductName(SettingsProductEnum::ARTIST_PREMIUM);
+        $this->assertEquals("Opentalent Artist Premium", $result);
+    }
+
+    /**
+     * @see DolibarrUtils::getDolibarrProductName()
+     */
+    public function testGetDolibarrProductNameForSchool(): void
+    {
+        $result = $this->dolibarrUtils->getDolibarrProductName(SettingsProductEnum::SCHOOL);
+        $this->assertEquals("Opentalent School", $result);
+    }
+
+    /**
+     * @see DolibarrUtils::getDolibarrProductName()
+     */
+    public function testGetDolibarrProductNameForSchoolPremiumTrial(): void
+    {
+        $result = $this->dolibarrUtils->getDolibarrProductName(SettingsProductEnum::SCHOOL_PREMIUM, true);
+        $this->assertEquals("Opentalent School Premium (Essai)", $result);
+    }
+
+    /**
+     * @see DolibarrUtils::getDolibarrProductName()
+     */
+    public function testGetDolibarrProductNameForSchoolPremium(): void
+    {
+        $result = $this->dolibarrUtils->getDolibarrProductName(SettingsProductEnum::SCHOOL_PREMIUM);
+        $this->assertEquals("Opentalent School Premium", $result);
+    }
+
+    /**
+     * @see DolibarrUtils::getDolibarrProductName()
+     */
+    public function testGetDolibarrProductNameForManager(): void
+    {
+        $result = $this->dolibarrUtils->getDolibarrProductName(SettingsProductEnum::MANAGER);
+        $this->assertEquals("Opentalent Manager", $result);
+    }
+
+    /**
+     * @see DolibarrUtils::getDolibarrProductName()
+     */
+    public function testGetDolibarrProductNameForManagerPremium(): void
+    {
+        $result = $this->dolibarrUtils->getDolibarrProductName(SettingsProductEnum::MANAGER_PREMIUM);
+        $this->assertEquals("Opentalent Manager Premium", $result);
+    }
+}

+ 91 - 0
tests/Unit/Service/Organization/TrialTest.php

@@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Tests\Unit\Service\Organization;
+
+use App\Service\Organization\Trial;
+use App\Service\Utils\DatesUtils;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Unit tests for Trial class
+ * @see Trial
+ */
+class TrialTest extends TestCase
+{
+    private DatesUtils $datesUtils;
+    private Trial $trial;
+
+    public function setUp(): void
+    {
+        $this->datesUtils = new DatesUtils();
+        $this->trial = new Trial($this->datesUtils);
+    }
+
+    public function tearDown(): void
+    {
+        DatesUtils::clearFakeDatetime();
+    }
+
+    /**
+     * @see Trial::getTrialCountdown()
+     */
+    public function testGetTrialCountdownWithNullStartDate(): void
+    {
+        $result = $this->trial->getTrialCountdown(null);
+
+        $this->assertEquals(0, $result);
+    }
+
+    /**
+     * @see Trial::getTrialCountdown()
+     */
+    public function testGetTrialCountdownWithRecentStartDate(): void
+    {
+        // Set up a trial start date
+        $trialStartDate = new \DateTime('2023-01-01');
+
+        // Set the current date to be 10 days after the trial start date
+        DatesUtils::setFakeDatetime('2023-01-11');
+
+        $result = $this->trial->getTrialCountdown($trialStartDate);
+
+        // Should return 30 - 10 = 20 days remaining
+        $this->assertEquals(20, $result);
+    }
+
+    /**
+     * @see Trial::getTrialCountdown()
+     */
+    public function testGetTrialCountdownWithExactly30DaysAgo(): void
+    {
+        // Set up a trial start date
+        $trialStartDate = new \DateTime('2023-01-01');
+
+        // Set the current date to be 30 days after the trial start date
+        DatesUtils::setFakeDatetime('2023-01-31');
+
+        $result = $this->trial->getTrialCountdown($trialStartDate);
+
+        // Should return 30 - 30 = 0 days remaining
+        $this->assertEquals(0, $result);
+    }
+
+    /**
+     * @see Trial::getTrialCountdown()
+     */
+    public function testGetTrialCountdownWithOldStartDate(): void
+    {
+        // Set up a trial start date
+        $trialStartDate = new \DateTime('2023-01-01');
+
+        // Set the current date to be 40 days after the trial start date
+        DatesUtils::setFakeDatetime('2023-02-10');
+
+        $result = $this->trial->getTrialCountdown($trialStartDate);
+
+        // Should return 0 days remaining since the trial has expired
+        $this->assertEquals(0, $result);
+    }
+}

+ 632 - 0
tests/Unit/Service/Shop/ShopServiceTest.php

@@ -0,0 +1,632 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Tests\Unit\Service\Shop;
+
+use App\ApiResources\Shop\NewStructureArtistPremiumTrialRequest;
+use App\Entity\Organization\Organization;
+use App\Entity\Organization\Settings;
+use App\Entity\Shop\ShopRequest;
+use App\Enum\Organization\LegalEnum;
+use App\Enum\Organization\PrincipalTypeEnum;
+use App\Enum\Organization\SettingsProductEnum;
+use App\Enum\Shop\ShopRequestStatus;
+use App\Enum\Shop\ShopRequestType;
+use App\Message\Message\Shop\NewStructureArtistPremiumTrial;
+use App\Service\Dolibarr\DolibarrApiService;
+use App\Service\Dolibarr\DolibarrUtils;
+use App\Service\Mailer\Mailer;
+use App\Service\Organization\OrganizationFactory;
+use App\Service\Shop\ShopService;
+use App\Service\Utils\DatesUtils;
+use Doctrine\ORM\EntityManagerInterface;
+use libphonenumber\PhoneNumberUtil;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Messenger\Envelope;
+use Symfony\Component\Messenger\MessageBusInterface;
+use Symfony\Component\Serializer\SerializerInterface;
+
+class TestableShopService extends ShopService
+{
+    public PhoneNumberUtil $phoneNumberUtil;
+
+    public function controlShopRequestData(ShopRequestType $type, array $data): void
+    {
+        parent::controlShopRequestData($type, $data);
+    }
+
+    public function createRequest(ShopRequestType $type, array $data): ShopRequest
+    {
+        return parent::createRequest($type, $data);
+    }
+
+    public function sendRequestValidationLink(ShopRequest $shopRequest): void
+    {
+        parent::sendRequestValidationLink($shopRequest);
+    }
+
+    public function startArtistPremiumTrial(Organization $organization, NewStructureArtistPremiumTrialRequest $request): void
+    {
+        parent::startArtistPremiumTrial($organization, $request);
+    }
+
+    public function handleNewStructureArtistPremiumTrialRequest(string $token): void
+    {
+        parent::handleNewStructureArtistPremiumTrialRequest($token);
+    }
+
+    public function createOrganization(NewStructureArtistPremiumTrialRequest $trialRequest): Organization
+    {
+        return parent::createOrganization($trialRequest);
+    }
+
+    public function generateSubdomain(string $name): string
+    {
+        return parent::generateSubdomain($name);
+    }
+
+    public function validateNewStructureArtistPremiumTrialRequest($request): void
+    {
+        parent::validateNewStructureArtistPremiumTrialRequest($request);
+    }
+}
+
+
+/**
+ * Unit tests for ShopService
+ */
+class ShopServiceTest extends TestCase
+{
+    private readonly MockObject|EntityManagerInterface $entityManager;
+    private readonly MockObject|Mailer $mailer;
+    private string $publicBaseUrl;
+    private readonly MockObject|OrganizationFactory $organizationFactory;
+    private readonly MockObject|SerializerInterface $serializer;
+    private readonly MockObject|LoggerInterface $logger;
+    private readonly MockObject|DolibarrApiService $dolibarrApiService;
+    private readonly MockObject|DolibarrUtils $dolibarrUtils;
+    private readonly MockObject|MessageBusInterface $messageBus;
+
+    public function setUp(): void
+    {
+        $this->entityManager = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
+        $this->mailer = $this->getMockBuilder(Mailer::class)->disableOriginalConstructor()->getMock();
+        $this->publicBaseUrl = 'https://example.com';
+        $this->organizationFactory = $this->getMockBuilder(OrganizationFactory::class)->disableOriginalConstructor()->getMock();
+        $this->serializer = $this->getMockBuilder(SerializerInterface::class)->disableOriginalConstructor()->getMock();
+        $this->logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock();
+        $this->dolibarrApiService = $this->getMockBuilder(DolibarrApiService::class)->disableOriginalConstructor()->getMock();
+        $this->dolibarrUtils = $this->getMockBuilder(DolibarrUtils::class)->disableOriginalConstructor()->getMock();
+        $this->messageBus = $this->getMockBuilder(MessageBusInterface::class)->disableOriginalConstructor()->getMock();
+    }
+
+    private function getShopServiceMockFor(string $methodName): TestableShopService|MockObject
+    {
+        $shopService = $this
+            ->getMockBuilder(TestableShopService::class)
+            ->setConstructorArgs(
+                [
+                    $this->entityManager,
+                    $this->mailer,
+                    $this->publicBaseUrl,
+                    $this->organizationFactory,
+                    $this->serializer,
+                    $this->logger,
+                    $this->dolibarrApiService,
+                    $this->dolibarrUtils,
+                    $this->messageBus,
+                ]
+            )
+            ->setMethodsExcept([$methodName])
+            ->getMock();
+
+        return $shopService;
+    }
+
+    public function tearDown(): void
+    {
+        DatesUtils::clearFakeDatetime();
+    }
+
+    /**
+     * Test registerNewShopRequest method
+     */
+    public function testRegisterNewShopRequest(): void
+    {
+        $shopService = $this->getShopServiceMockFor('registerNewShopRequest');
+
+        $type = ShopRequestType::NEW_STRUCTURE_ARTIST_PREMIUM_TRIAL;
+        $data = [
+            'representativeEmail' => 'test@example.com',
+            'representativeFirstName' => 'John',
+            'representativeLastName' => 'Doe',
+            'structureName' => 'Test Structure',
+        ];
+
+        $shopRequest = $this->getMockBuilder(ShopRequest::class)->getMock();
+
+        $shopService->expects(self::once())
+            ->method('controlShopRequestData')
+            ->with($type, $data);
+
+        $shopService->expects(self::once())
+            ->method('createRequest')
+            ->with($type, $data)
+            ->willReturn($shopRequest);
+
+        $shopService->expects(self::once())
+            ->method('sendRequestValidationLink')
+            ->with($shopRequest);
+
+        $result = $shopService->registerNewShopRequest($type, $data);
+
+        $this->assertSame($shopRequest, $result);
+    }
+
+    /**
+     * Test controlShopRequestData method for NEW_STRUCTURE_ARTIST_PREMIUM_TRIAL type
+     */
+    public function testControlShopRequestDataForNewStructureArtistPremiumTrial(): void
+    {
+        $shopService = $this->getShopServiceMockFor('controlShopRequestData');
+
+        $type = ShopRequestType::NEW_STRUCTURE_ARTIST_PREMIUM_TRIAL;
+        $data = [
+            'representativeEmail' => 'test@example.com',
+            'representativeFirstName' => 'John',
+            'representativeLastName' => 'Doe',
+            'structureName' => 'Test Structure',
+        ];
+
+        $shopService->expects(self::once())
+            ->method('validateNewStructureArtistPremiumTrialRequest')
+            ->with($data);
+
+        $shopService->controlShopRequestData($type, $data);
+    }
+
+    /**
+     * Test processShopRequest method for NEW_STRUCTURE_ARTIST_PREMIUM_TRIAL type
+     */
+    public function testProcessShopRequest(): void
+    {
+        $shopService = $this->getShopServiceMockFor('processShopRequest');
+
+        $shopRequest = $this->getMockBuilder(ShopRequest::class)->getMock();
+        $shopRequest->method('getType')->willReturn(ShopRequestType::NEW_STRUCTURE_ARTIST_PREMIUM_TRIAL);
+        $shopRequest->method('getToken')->willReturn('test-token');
+
+        $this->messageBus
+            ->expects(self::once())
+            ->method('dispatch')
+            ->with(self::callback(function ($message) {
+                return $message instanceof NewStructureArtistPremiumTrial
+                    && $message->getToken() === 'test-token';
+            }))
+            ->willReturn(new Envelope(new NewStructureArtistPremiumTrial('azerty')));
+
+        $shopRequest
+            ->expects(self::once())
+            ->method('setStatus')
+            ->with(ShopRequestStatus::VALIDATED);
+
+        $this->entityManager->expects(self::once())
+            ->method('persist')
+            ->with($shopRequest);
+
+        $this->entityManager->expects(self::once())
+            ->method('flush');
+
+        $shopService->processShopRequest($shopRequest);
+    }
+
+    /**
+     * Test createRequest method
+     */
+    public function testCreateRequest(): void
+    {
+        $shopService = $this->getShopServiceMockFor('createRequest');
+
+        $uuidRx = "/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i";
+
+        $type = ShopRequestType::NEW_STRUCTURE_ARTIST_PREMIUM_TRIAL;
+        $data = [
+            'representativeEmail' => 'test@example.com',
+            'representativeFirstName' => 'John',
+            'representativeLastName' => 'Doe',
+            'structureName' => 'Test Structure',
+        ];
+
+        $this->entityManager->expects(self::once())
+            ->method('persist')
+            ->with(self::callback(function ($shopRequest) use ($type, $data, $uuidRx) {
+                return $shopRequest instanceof ShopRequest
+                    && $shopRequest->getType() === $type
+                    && $shopRequest->getData() === $data
+                    && preg_match($uuidRx, $shopRequest->getToken());
+            }));
+
+        $this->entityManager->expects(self::once())
+            ->method('flush');
+
+        $result = $shopService->createRequest($type, $data);
+
+        $this->assertInstanceOf(ShopRequest::class, $result);
+        $this->assertSame($type, $result->getType());
+        $this->assertSame($data, $result->getData());
+        $this->assertMatchesRegularExpression($uuidRx, $result->getToken());
+    }
+
+    /**
+     * Test sendRequestValidationLink method
+     */
+    public function testSendRequestValidationLink(): void
+    {
+        $shopService = $this->getShopServiceMockFor('sendRequestValidationLink');
+
+        $token = 'test-token';
+        $data = [
+            'representativeEmail' => 'test@example.com',
+            'representativeFirstName' => 'John',
+            'representativeLastName' => 'Doe',
+            'structureName' => 'Test Structure',
+        ];
+
+        $shopRequest = $this->getMockBuilder(ShopRequest::class)->getMock();
+        $shopRequest->method('getToken')->willReturn($token);
+        $shopRequest->method('getData')->willReturn($data);
+
+        $this->mailer
+            ->expects(self::once())
+            ->method('main')
+            ->with(self::callback(function ($model) use ($token, $data) {
+                var_dump($model);
+                return $model->getToken() === $token
+                    && $model->getRepresentativeEmail() === $data['representativeEmail']
+                    && $model->getRepresentativeFirstName() === $data['representativeFirstName']
+                    && $model->getRepresentativeLastName() === $data['representativeLastName']
+                    && $model->getStructureName() === $data['structureName']
+                    && $model->getValidationUrl() === 'https://example.com/api/public/shop/validate/' . $token;
+            }));
+
+        $shopRequest->expects(self::once())
+            ->method('setStatus')
+            ->with(ShopRequestStatus::ACTIVATION_LINK_SENT);
+
+        $this->entityManager->expects(self::once())
+            ->method('persist')
+            ->with($shopRequest);
+
+        $this->entityManager->expects(self::once())
+            ->method('flush');
+
+        $shopService->sendRequestValidationLink($shopRequest);
+    }
+
+    /**
+     * Test startArtistPremiumTrial method
+     */
+    public function testStartArtistPremiumTrial(): void
+    {
+        $shopService = $this->getShopServiceMockFor('startArtistPremiumTrial');
+
+        DatesUtils::setFakeDatetime('2025-01-01 12:00:00');
+
+        $organization = $this->getMockBuilder(Organization::class)->getMock();
+
+        $settings = $this->getMockBuilder(Settings::class)->getMock();
+        $settings->method('getProduct')->willReturn(SettingsProductEnum::FREEMIUM);
+
+        $organization->method('getSettings')->willReturn($settings);
+        $organization->method('getId')->willReturn(123);
+
+        $request = $this->getMockBuilder(NewStructureArtistPremiumTrialRequest::class)->getMock();
+        $request->method('getRepresentativeFirstName')->willReturn('John');
+        $request->method('getRepresentativeLastName')->willReturn('Doe');
+        $request->method('getRepresentativeFunction')->willReturn('Manager');
+        $request->method('getRepresentativeEmail')->willReturn('test@example.com');
+        $request->method('getRepresentativePhone')->willReturn('+33123456789');
+
+        $settings
+            ->expects(self::once())
+            ->method('setProductBeforeTrial')
+            ->with(SettingsProductEnum::FREEMIUM);
+
+        $settings
+            ->expects(self::once())
+            ->method('setTrialActive')
+            ->with(true);
+
+        $settings
+            ->expects(self::once())
+            ->method('setLastTrialStartDate')
+            ->with(self::callback(function ($dateTime) {
+                return $dateTime instanceof \DateTime && $dateTime->format('Y-m-d H:i:s') === '2025-01-01 12:00:00';
+            }));
+
+        $settings->expects(self::once())
+            ->method('setProduct')
+            ->with(SettingsProductEnum::ARTIST_PREMIUM);
+
+        $this->entityManager->expects(self::once())
+            ->method('persist')
+            ->with($settings);
+
+        $this->entityManager->expects(self::once())
+            ->method('flush');
+
+        $this->dolibarrApiService->expects(self::once())
+            ->method('getSocietyId')
+            ->with(123)
+            ->willReturn(456);
+
+        $this->dolibarrUtils->expects(self::once())
+            ->method('getProductId')
+            ->with(SettingsProductEnum::ARTIST_PREMIUM, true)
+            ->willReturn(789);
+
+        $this->dolibarrApiService->expects(self::once())
+            ->method('createContract')
+            ->with(456, 789, true, 1)
+            ->willReturn(101112);
+
+        $this->dolibarrApiService->expects(self::once())
+            ->method('createContractLine')
+            ->with(101112, 789, 1);
+
+        $this->dolibarrUtils->expects(self::once())
+            ->method('updateSocietyCommercialsWithApi')
+            ->with(456);
+
+        $this->dolibarrUtils->expects(self::once())
+            ->method('getDolibarrProductName')
+            ->with(SettingsProductEnum::ARTIST_PREMIUM, true)
+            ->willReturn('Artist Premium (essai)');
+
+        $this->dolibarrApiService->expects(self::once())
+            ->method('updateSocietyProduct')
+            ->with(456, 'Artist Premium (essai)');
+
+        $this->dolibarrUtils->expects(self::once())
+            ->method('addActionComm')
+            ->with(456, "Ouverture de la période d'essai", self::callback(function ($message) {
+                return strpos($message, 'John') !== false
+                    && strpos($message, 'Doe') !== false
+                    && strpos($message, 'Manager') !== false
+                    && strpos($message, 'test@example.com') !== false
+                    && strpos($message, '+33123456789') !== false;
+            }));
+
+        $shopService->startArtistPremiumTrial($organization, $request);
+
+        // Clear the fake date time after the test
+        DatesUtils::clearFakeDatetime();
+    }
+
+    /**
+     * Test handleNewStructureArtistPremiumTrialRequest method
+     */
+    public function testHandleNewStructureArtistPremiumTrialRequest(): void
+    {
+        $shopService = $this->getShopServiceMockFor('handleNewStructureArtistPremiumTrialRequest');
+
+        $token = 'test-token';
+        $data = [
+            'representativeEmail' => 'test@example.com',
+            'representativeFirstName' => 'John',
+            'representativeLastName' => 'Doe',
+            'structureName' => 'Test Structure',
+        ];
+
+        $shopRequest = $this->getMockBuilder(ShopRequest::class)->getMock();
+        $shopRequest->method('getToken')->willReturn($token);
+        $shopRequest->method('getData')->willReturn($data);
+
+        $this->entityManager
+            ->expects(self::once())
+            ->method('find')
+            ->with(ShopRequest::class, $token)
+            ->willReturn($shopRequest);
+
+        $trialRequest = $this->getMockBuilder(\App\ApiResources\Shop\NewStructureArtistPremiumTrialRequest::class)
+            ->getMock();
+
+        $this->serializer
+            ->expects(self::once())
+            ->method('deserialize')
+            ->with(json_encode($data), NewStructureArtistPremiumTrialRequest::class, 'json')
+            ->willReturn($trialRequest);
+
+        $organization = $this->getMockBuilder(Organization::class)->getMock();
+
+        $shopService
+            ->expects(self::once())
+            ->method('createOrganization')
+            ->with($trialRequest)
+            ->willReturn($organization);
+
+        $shopService
+            ->expects(self::once())
+            ->method('startArtistPremiumTrial')
+            ->with($organization, $trialRequest);
+
+        $this->logger->expects(self::once())
+            ->method('info')
+            ->with('Successfully processed NewStructureArtistPremiumTrial for token: ' . $token);
+
+        $shopService->handleNewStructureArtistPremiumTrialRequest($token);
+    }
+
+    /**
+     * Test handleNewStructureArtistPremiumTrialRequest method with non-existent token
+     */
+    public function testHandleNewStructureArtistPremiumTrialRequestWithNonExistentToken(): void
+    {
+        $shopService = $this->getShopServiceMockFor('handleNewStructureArtistPremiumTrialRequest');
+
+        $this->entityManager->expects(self::once())
+            ->method('find')
+            ->with(ShopRequest::class, 'non-existent-token')
+            ->willReturn(null);
+
+        $this->logger->expects(self::once())
+            ->method('error')
+            ->with('Cannot find ShopRequest with token: non-existent-token');
+
+        $shopService->handleNewStructureArtistPremiumTrialRequest('non-existent-token');
+    }
+
+    /**
+     * Test createOrganization method
+     */
+    public function testCreateOrganization(): void
+    {
+        $shopService = $this->getShopServiceMockFor('createOrganization');
+
+        $trialRequest = $this
+            ->getMockBuilder(NewStructureArtistPremiumTrialRequest::class)
+            ->getMock();
+
+        $trialRequest->method('getStructureName')->willReturn('Test Structure');
+        $trialRequest->method('getAddress')->willReturn('123 Main St');
+        $trialRequest->method('getAddressComplement')->willReturn('Suite 456');
+        $trialRequest->method('getPostalCode')->willReturn('75000');
+        $trialRequest->method('getCity')->willReturn('Paris');
+        $trialRequest->method('getStructureEmail')->willReturn('structure@example.com');
+        $trialRequest->method('getStructureType')->willReturn(PrincipalTypeEnum::ARTISTIC_EDUCATION_ONLY);
+        $trialRequest->method('getLegalStatus')->willReturn(LegalEnum::ASSOCIATION_LAW_1901);
+        $trialRequest->method('getSiren')->willReturn('123456789');
+        $trialRequest->method('getRepresentativePhone')->willReturn('+33123456789');
+
+        $shopService
+            ->expects(self::once())
+            ->method('generateSubdomain')
+            ->with('Test Structure')
+            ->willReturn('test-structure');
+
+        $organization = $this->getMockBuilder(Organization::class)->getMock();
+
+        // Mock the organizationFactory.create method to return the mock Organization
+        $this->organizationFactory
+            ->expects(self::once())
+            ->method('create')
+            ->with(self::callback(function ($organizationCreationRequest) {
+                return $organizationCreationRequest->getName() === 'Test Structure'
+                    && $organizationCreationRequest->getStreetAddress1() === '123 Main St'
+                    && $organizationCreationRequest->getStreetAddress2() === 'Suite 456'
+                    && $organizationCreationRequest->getPostalCode() === '75000'
+                    && $organizationCreationRequest->getCity() === 'Paris'
+                    && $organizationCreationRequest->getEmail() === 'structure@example.com'
+                    && $organizationCreationRequest->getPrincipalType() === PrincipalTypeEnum::ARTISTIC_EDUCATION_ONLY
+                    && $organizationCreationRequest->getLegalStatus() === LegalEnum::ASSOCIATION_LAW_1901
+                    && $organizationCreationRequest->getSiretNumber() === '123456789'
+                    && $organizationCreationRequest->getPhoneNumber() === '+33123456789'
+                    && $organizationCreationRequest->getSubdomain() === 'test-structure'
+                    && $organizationCreationRequest->getProduct()->value === SettingsProductEnum::FREEMIUM->value
+                    && $organizationCreationRequest->getCreateWebsite() === false
+                    && $organizationCreationRequest->isClient() === false;
+            }))
+            ->willReturn($organization);
+
+        $result = $shopService->createOrganization($trialRequest);
+
+        $this->assertSame($organization, $result);
+    }
+
+    /**
+     * Test generateSubdomain method
+     */
+    public function testGenerateSubdomain(): void
+    {
+        $shopService = $this->getShopServiceMockFor('generateSubdomain');
+
+        // Test with a simple name
+        $this->assertEquals('test-structure', $shopService->generateSubdomain('Test Structure'));
+
+        // Test with accents and special characters
+        $this->assertEquals('ecole-de-musique', $shopService->generateSubdomain('École de Musique'));
+
+        // Test with multiple spaces and special characters
+        $this->assertEquals('conservatoire-regional', $shopService->generateSubdomain('Conservatoire  Régional!@#'));
+
+        // Test with a very long name (should be truncated to 30 characters)
+        $longName = 'This is a very long name that should be truncated';
+        $this->assertEquals('this-is-a-very-long-name-that', $shopService->generateSubdomain($longName));
+
+        // Test with leading and trailing hyphens (should be trimmed)
+        $this->assertEquals('trimmed', $shopService->generateSubdomain('--Trimmed--'));
+
+        // Test with consecutive special characters (should be replaced with a single hyphen)
+        $this->assertEquals('multiple-hyphens', $shopService->generateSubdomain('Multiple!!!Hyphens'));
+    }
+
+    /**
+     * Test validateNewStructureArtistPremiumTrialRequest method with valid data
+     */
+    public function testValidateNewStructureArtistPremiumTrialRequest(): void
+    {
+        $shopService = $this->getShopServiceMockFor('validateNewStructureArtistPremiumTrialRequest');
+
+        $phoneNumberUtil = $this
+            ->getMockBuilder(PhoneNumberUtil::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $shopService->phoneNumberUtil = $phoneNumberUtil;
+
+        $request = new NewStructureArtistPremiumTrialRequest();
+        $request->setStructureName('Test Structure');
+        $request->setCity('Test City');
+        $request->setPostalCode('12345');
+        $request->setAddress('Test Address');
+        $request->setAddressComplement('Test Address Complement');
+        $request->setRepresentativePhone('+33123456789');
+
+        $phoneNumberUtil->expects(self::once())
+            ->method('isPossibleNumber')
+            ->with('+33123456789')
+            ->willReturn(true);
+
+        $this->organizationFactory->expects(self::once())
+            ->method('interruptIfOrganizationExists')
+            ->with(self::callback(function ($organizationCreationRequest) {
+                return $organizationCreationRequest->getName() === 'Test Structure'
+                    && $organizationCreationRequest->getCity() === 'Test City'
+                    && $organizationCreationRequest->getPostalCode() === '12345'
+                    && $organizationCreationRequest->getStreetAddress1() === 'Test Address'
+                    && $organizationCreationRequest->getStreetAddress2() === 'Test Address Complement'
+                    && $organizationCreationRequest->getStreetAddress3() === '';
+            }));
+
+        $shopService->validateNewStructureArtistPremiumTrialRequest($request);
+    }
+
+    /**
+     * Test validateNewStructureArtistPremiumTrialRequest method with invalid phone number
+     */
+    public function testValidateNewStructureArtistPremiumTrialRequestWithInvalidPhoneNumber(): void
+    {
+        $shopService = $this->getShopServiceMockFor('validateNewStructureArtistPremiumTrialRequest');
+
+        $phoneNumberUtil = $this->getMockBuilder(PhoneNumberUtil::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $shopService->phoneNumberUtil = $phoneNumberUtil;
+
+        $request = new NewStructureArtistPremiumTrialRequest();
+        $request->setRepresentativePhone('invalid-phone');
+
+        // Mock the phoneNumberUtil to return false for isPossibleNumber
+        $phoneNumberUtil->expects(self::once())
+            ->method('isPossibleNumber')
+            ->with('invalid-phone')
+            ->willReturn(false);
+
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionMessage('Invalid phone number');
+
+        $shopService->validateNewStructureArtistPremiumTrialRequest($request);
+    }
+}