From c623a8ceda4f8b47aa3371f8527e021fd8776ad7 Mon Sep 17 00:00:00 2001 From: Matthias-Kelvin Daous Date: Fri, 22 Aug 2025 18:20:25 +0200 Subject: [PATCH 01/17] Add manufacturer endpoint --- .../Resources/Manufacturer/Manufacturer.php | 138 +++++++ .../Manufacturer/ManufacturerDetail.php | 71 ++++ .../Manufacturer/ManufacturerList.php | 67 ++++ .../ApiPlatform/ManufacturerEndpointTest.php | 345 ++++++++++++++++++ 4 files changed, 621 insertions(+) create mode 100644 src/ApiPlatform/Resources/Manufacturer/Manufacturer.php create mode 100644 src/ApiPlatform/Resources/Manufacturer/ManufacturerDetail.php create mode 100644 src/ApiPlatform/Resources/Manufacturer/ManufacturerList.php create mode 100644 tests/Integration/ApiPlatform/ManufacturerEndpointTest.php diff --git a/src/ApiPlatform/Resources/Manufacturer/Manufacturer.php b/src/ApiPlatform/Resources/Manufacturer/Manufacturer.php new file mode 100644 index 00000000..fec381fa --- /dev/null +++ b/src/ApiPlatform/Resources/Manufacturer/Manufacturer.php @@ -0,0 +1,138 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\Manufacturer; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\Manufacturer\Command\AddManufacturerCommand; +use PrestaShop\PrestaShop\Core\Domain\Manufacturer\Command\DeleteManufacturerCommand; +use PrestaShop\PrestaShop\Core\Domain\Manufacturer\Command\EditManufacturerCommand; +use PrestaShop\PrestaShop\Core\Domain\Manufacturer\Exception\ManufacturerException; +use PrestaShop\PrestaShop\Core\Domain\Manufacturer\Exception\ManufacturerNotFoundException; +use PrestaShop\PrestaShop\Core\Domain\Manufacturer\Query\GetManufacturerForEditing; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSCreate; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSDelete; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSPartialUpdate; +use PrestaShopBundle\ApiPlatform\Metadata\LocalizedValue; +use Symfony\Component\HttpFoundation\Response; + +#[ApiResource( + operations: [ + // GET /manufacturer/{manufacturerId} + new CQRSGet( + uriTemplate: '/manufacturer/{manufacturerId}', + requirements: ['manufacturerId' => '\d+'], + CQRSQuery: GetManufacturerForEditing::class, + scopes: ['manufacturer_read'], + CQRSQueryMapping: self::QUERY_MAPPING, + ), + // POST /manufacturer + new CQRSCreate( + uriTemplate: '/manufacturer', + CQRSCommand: AddManufacturerCommand::class, + CQRSQuery: GetManufacturerForEditing::class, + scopes: ['manufacturer_write'], + CQRSQueryMapping: self::QUERY_MAPPING, + CQRSCommandMapping: self::COMMAND_MAPPING, + ), + new CQRSPartialUpdate( + uriTemplate: '/manufacturer/{manufacturerId}', + requirements: ['manufacturerId' => '\d+'], + CQRSCommand: EditManufacturerCommand::class, + CQRSQuery: GetManufacturerForEditing::class, + scopes: ['manufacturer_write'], + CQRSQueryMapping: self::QUERY_MAPPING, + CQRSCommandMapping: self::COMMAND_MAPPING, + ), + new CQRSDelete( + uriTemplate: '/manufacturer/{manufacturerId}', + requirements: ['manufacturerId' => '\d+'], + CQRSCommand: DeleteManufacturerCommand::class, + scopes: ['manufacturer_write'], + ), + ], + exceptionToStatus: [ + ManufacturerException::class => Response::HTTP_UNPROCESSABLE_ENTITY, + ManufacturerNotFoundException::class => Response::HTTP_NOT_FOUND, + ], +)] +class Manufacturer +{ + #[ApiProperty(identifier: true)] + public int $manufacturerId; + + public string $name; + + #[ApiProperty( + required: false, + openapiContext: ['nullable' => true] + )] + public ?array $logoImage = null; + + #[LocalizedValue] + public array $shortDescriptions; + + #[LocalizedValue] + public array $descriptions; + + #[LocalizedValue] + public array $metaTitles; + + #[LocalizedValue] + public array $metaDescriptions; + + #[LocalizedValue] + public array $metaKeywords; + + #[ApiProperty(openapiContext: ['type' => 'array', 'items' => ['type' => 'integer']])] + public array $shopIds; + + public bool $enabled; + + public const QUERY_MAPPING = [ + '[manufacturerId]' => '[manufacturerId]', + '[name]' => '[name]', + '[logoImage]' => '[logoImage]', + '[localizedShortDescriptions]' => '[shortDescriptions]', + '[localizedMetaTitles]' => '[metaTitles]', + '[localizedDescriptions]' => '[descriptions]', + '[localizedMetaDescriptions]' => '[metaDescriptions]', + '[localizedMetaKeywords]' => '[metaKeywords]', + '[enabled]' => '[enabled]', + '[associatedShops]' => '[shopIds]', + ]; + + public const COMMAND_MAPPING = [ + '[manufacturerId]' => '[manufacturerId]', + '[name]' => '[name]', + '[logoImage]' => '[logoImage]', + '[shortDescriptions]' => '[localizedShortDescriptions]', + '[metaTitles]' => '[localizedMetaTitles]', + '[descriptions]' => '[localizedDescriptions]', + '[metaDescriptions]' => '[localizedMetaDescriptions]', + '[enabled]' => '[enabled]', + '[shopIds]' => '[shopAssociation]', + ]; +} diff --git a/src/ApiPlatform/Resources/Manufacturer/ManufacturerDetail.php b/src/ApiPlatform/Resources/Manufacturer/ManufacturerDetail.php new file mode 100644 index 00000000..afba110e --- /dev/null +++ b/src/ApiPlatform/Resources/Manufacturer/ManufacturerDetail.php @@ -0,0 +1,71 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\Manufacturer; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\Manufacturer\Exception\ManufacturerConstraintException; +use PrestaShop\PrestaShop\Core\Domain\Manufacturer\Exception\ManufacturerNotFoundException; +use PrestaShop\PrestaShop\Core\Domain\Manufacturer\Query\GetManufacturerForViewing; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; +use Symfony\Component\HttpFoundation\Response; + +#[ApiResource( + operations: [ + // GET /manufacturer/{manufacturerId}/details/{languageId} + new CQRSGet( + uriTemplate: '/manufacturer/{manufacturerId}/details/{languageId}', + requirements: [ + 'manufacturerId' => '\d+', + 'languageId' => '\d+', + ], + CQRSQuery: GetManufacturerForViewing::class, + scopes: ['manufacturer_read'], + CQRSQueryMapping: self::QUERY_MAPPING, + ), + ], + exceptionToStatus: [ + ManufacturerConstraintException::class => Response::HTTP_UNPROCESSABLE_ENTITY, + ManufacturerNotFoundException::class => Response::HTTP_NOT_FOUND, + ], +)] +class ManufacturerDetail +{ + #[ApiProperty(identifier: true, readable: false)] + public int $manufacturerId = 0; + + #[ApiProperty(identifier: true, readable: false)] + public int $languageId = 0; + + public string $name; + + public array $products = []; + + public array $addresses = []; + + public const QUERY_MAPPING = [ + 'manufacturerProducts' => 'products', + 'manufacturerAddresses' => 'addresses', + ]; +} diff --git a/src/ApiPlatform/Resources/Manufacturer/ManufacturerList.php b/src/ApiPlatform/Resources/Manufacturer/ManufacturerList.php new file mode 100644 index 00000000..f5d573ff --- /dev/null +++ b/src/ApiPlatform/Resources/Manufacturer/ManufacturerList.php @@ -0,0 +1,67 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\Manufacturer; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShopBundle\ApiPlatform\Metadata\PaginatedList; + +#[ApiResource( + operations: [ + new PaginatedList( + uriTemplate: '/manufacturers', + scopes: [ + 'manufacturer_read', + ], + ApiResourceMapping: self::MAPPING, + gridDataFactory: 'prestashop.core.grid.data.factory.manufacturer_decorator', + filtersMapping: [ + '[manufacturerId]' => '[id_manufacturer]', + ], + ), + ] +)] +class ManufacturerList +{ + #[ApiProperty(identifier: true)] + public int $manufacturerId; + + public string $name; + + public ?string $logo; + + public int $productCount; + + public int|string $addressesCount; + + public bool $enabled; + + public const MAPPING = [ + '[id_manufacturer]' => '[manufacturerId]', + '[name]' => '[name]', + '[product_count]' => '[productCount]', + '[addresses_count]' => '[addressesCount]', + '[active]' => '[enabled]', + ]; +} diff --git a/tests/Integration/ApiPlatform/ManufacturerEndpointTest.php b/tests/Integration/ApiPlatform/ManufacturerEndpointTest.php new file mode 100644 index 00000000..c7746b7b --- /dev/null +++ b/tests/Integration/ApiPlatform/ManufacturerEndpointTest.php @@ -0,0 +1,345 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +declare(strict_types=1); + +namespace PsApiResourcesTest\Integration\ApiPlatform; + +use Symfony\Component\HttpFoundation\Response; +use Tests\Resources\DatabaseDump; +use Tests\Resources\Resetter\LanguageResetter; + +class ManufacturerEndpointTest extends ApiTestCase +{ + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + // Add the fr-FR language to test multi lang values accurately + LanguageResetter::resetLanguages(); + self::addLanguageByLocale('fr-FR'); + self::resetTables(); + // Pre-create the API Client with the needed scopes, this way we reduce the number of created API Clients + self::createApiClient(['manufacturer_write', 'manufacturer_read']); + } + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + // Reset DB as it was before this test + LanguageResetter::resetLanguages(); + self::resetTables(); + } + + protected static function resetTables(): void + { + DatabaseDump::restoreTables([ + 'manufacturer', + 'manufacturer_lang', + 'manufacturer_shop', + ]); + } + + public function getProtectedEndpoints(): iterable + { + yield 'get endpoint' => [ + 'GET', + '/manufacturer/1', + ]; + + yield 'get details endpoint' => [ + 'GET', + '/manufacturer/1/details/1', + ]; + + yield 'create endpoint' => [ + 'POST', + '/manufacturer', + ]; + + yield 'patch endpoint' => [ + 'PATCH', + '/manufacturer/1', + ]; + + yield 'delete endpoint' => [ + 'DELETE', + '/manufacturer/1', + ]; + } + + public function testAddManufacturer(): int + { + $itemsCount = $this->countItems('/manufacturers', ['manufacturer_read']); + $postData = [ + 'name' => 'manufacturer name', + 'shortDescriptions' => [ + 'en-US' => 'short description en', + 'fr-FR' => 'short description fr', + ], + 'descriptions' => [ + 'en-US' => 'description en', + 'fr-FR' => 'description fr', + ], + 'metaTitles' => [ + 'en-US' => 'meta title en', + 'fr-FR' => 'meta title fr', + ], + 'metaDescriptions' => [ + 'en-US' => 'meta description en', + 'fr-FR' => 'meta description fr', + ], + 'shopIds' => [1], + 'enabled' => true, + ]; + // Create an manufacturer, the POST endpoint returns the created item as JSON + $manufacturer = $this->createItem('/manufacturer', $postData, ['manufacturer_write']); + $this->assertArrayHasKey('manufacturerId', $manufacturer); + $manufacturerId = $manufacturer['manufacturerId']; + + // We assert the returned data matches what was posted (plus the ID) + $this->assertEquals( + ['manufacturerId' => $manufacturerId] + $postData, + $manufacturer + ); + + $newItemsCount = $this->countItems('/manufacturers', ['manufacturer_read']); + $this->assertEquals($itemsCount + 1, $newItemsCount); + + return $manufacturerId; + } + + /** + * @depends testAddManufacturer + * + * @param int $manufacturerId + * + * @return int + */ + public function testGetManufacturer(int $manufacturerId): int + { + $manufacturer = $this->getItem('/manufacturer/' . $manufacturerId, ['manufacturer_read']); + $this->assertEquals([ + 'manufacturerId' => $manufacturerId, + 'name' => 'manufacturer name', + 'shortDescriptions' => [ + 'en-US' => 'short description en', + 'fr-FR' => 'short description fr', + ], + 'descriptions' => [ + 'en-US' => 'description en', + 'fr-FR' => 'description fr', + ], + 'metaTitles' => [ + 'en-US' => 'meta title en', + 'fr-FR' => 'meta title fr', + ], + 'metaDescriptions' => [ + 'en-US' => 'meta description en', + 'fr-FR' => 'meta description fr', + ], + 'shopIds' => [1], + 'enabled' => true, + ], $manufacturer); + + return $manufacturerId; + } + + /** + * @depends testGetManufacturer + * + * @param int $manufacturerId + * + * @return int + */ + public function testGetManufacturerDetails(int $manufacturerId): int + { + $manufacturer = $this->getItem('/manufacturer/' . $manufacturerId . '/details/1', ['manufacturer_read']); + + $this->assertEquals([ + 'name' => 'manufacturer name', + 'products' => [], + 'addresses' => [], + ], $manufacturer); + + return $manufacturerId; + } + + /** + * @depends testGetManufacturer + * + * @param int $manufacturerId + * + * @return int + */ + public function testUpdatePartialManufacturer(int $manufacturerId): int + { + $patchData = [ + 'name' => 'updated manufacturer', + 'enabled' => true, + 'shortDescriptions' => [ + 'en-US' => 'updated short desc en', + 'fr-FR' => 'updated short desc fr', + ], + 'descriptions' => [ + 'en-US' => 'updated description en', + 'fr-FR' => 'updated description fr', + ], + 'metaTitles' => [ + 'en-US' => 'updated meta title en', + 'fr-FR' => 'updated meta title en', + ], + 'metaDescriptions' => [ + 'en-US' => 'updated meta description en', + 'fr-FR' => 'updated meta description fr', + ], + 'shopIds' => [1], + ]; + + $updatedManufacturer = $this->partialUpdateItem('/manufacturer/' . $manufacturerId, $patchData, ['manufacturer_write']); + $this->assertEquals(['manufacturerId' => $manufacturerId] + $patchData, $updatedManufacturer); + + // We check that when we GET the item it is updated as expected + $manufacturer = $this->getItem('/manufacturer/' . $manufacturerId, ['manufacturer_read']); + $this->assertEquals(['manufacturerId' => $manufacturerId] + $patchData, $manufacturer); + + // Test partial update + $partialUpdateData = [ + 'name' => 'updated manufacturer name', + 'enabled' => false, + 'shortDescriptions' => [ + 'en-US' => 'updated short description en', + 'fr-FR' => 'updated short description fr', + ], + ]; + $expectedUpdatedData = [ + 'manufacturerId' => $manufacturerId, + 'name' => 'updated manufacturer name', + 'enabled' => false, + 'shortDescriptions' => [ + 'en-US' => 'updated short description en', + 'fr-FR' => 'updated short description fr', + ], + 'descriptions' => [ + 'en-US' => 'updated description en', + 'fr-FR' => 'updated description fr', + ], + 'metaTitles' => [ + 'en-US' => 'updated meta title en', + 'fr-FR' => 'updated meta title en', + ], + 'metaDescriptions' => [ + 'en-US' => 'updated meta description en', + 'fr-FR' => 'updated meta description fr', + ], + 'shopIds' => [1], + ]; + + $updatedManufacturer = $this->partialUpdateItem('/manufacturer/' . $manufacturerId, $partialUpdateData, ['manufacturer_write']); + $this->assertEquals($expectedUpdatedData, $updatedManufacturer); + + return $manufacturerId; + } + + /** + * @depends testUpdatePartialManufacturer + * + * @param int $manufacturerId + * + * @return int + */ + public function testListManufacturers(int $manufacturerId): int + { + // List by manufacturerId in descending order so the created one comes first (and test ordering at the same time) + $paginatedManufacturers = $this->listItems('/manufacturers?orderBy=manufacturerId&sortOrder=desc', ['manufacturer_read']); + $this->assertGreaterThanOrEqual(1, $paginatedManufacturers['totalItems']); + + // Check the details to make sure filters mapping is correct + $this->assertEquals('manufacturerId', $paginatedManufacturers['orderBy']); + + // Test manufacturer should be the first returned in the list + $testManufacturer = $paginatedManufacturers['items'][0]; + + $expectedManufacturer = [ + 'manufacturerId' => $manufacturerId, + 'name' => 'updated manufacturer name', + 'addressesCount' => '--', + 'enabled' => false, + ]; + $this->assertEquals($expectedManufacturer, $testManufacturer); + + $filteredManufacturers = $this->listItems('/manufacturers', ['manufacturer_read'], [ + 'manufacturerId' => $manufacturerId, + ]); + $this->assertEquals(1, $filteredManufacturers['totalItems']); + + $testManufacturer = $filteredManufacturers['items'][0]; + $this->assertEquals($expectedManufacturer, $testManufacturer); + + // Check the filters details + $this->assertEquals([ + 'manufacturerId' => $manufacturerId, + ], $filteredManufacturers['filters']); + + return $manufacturerId; + } + + public function testInvalidManufacturer(): void + { + $manufacturerInvalidData = [ + 'name' => 'updated manufacturer name', + 'enabled' => false, + 'shortDescriptions' => [ + //