diff --git a/src/ApiPlatform/Resources/Store/BulkStoresDelete.php b/src/ApiPlatform/Resources/Store/BulkStoresDelete.php new file mode 100644 index 00000000..dfeeb2b9 --- /dev/null +++ b/src/ApiPlatform/Resources/Store/BulkStoresDelete.php @@ -0,0 +1,57 @@ + + * @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\Store; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\Store\Command\BulkDeleteStoreCommand; +use PrestaShop\PrestaShop\Core\Domain\Store\Exception\StoreNotFoundException; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSUpdate; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Validator\Constraints as Assert; + +#[ApiResource( + operations: [ + new CQRSUpdate( + uriTemplate: '/stores/delete', + // No output 204 code + output: false, + CQRSCommand: BulkDeleteStoreCommand::class, + scopes: [ + 'store_write', + ], + ), + ], + exceptionToStatus: [ + StoreNotFoundException::class => Response::HTTP_NOT_FOUND, + ], +)] +class BulkStoresDelete +{ + /** + * @var int[] + */ + #[ApiProperty(openapiContext: ['type' => 'array', 'items' => ['type' => 'integer'], 'example' => [1, 3]])] + #[Assert\NotBlank] + public array $storeIds; +} diff --git a/src/ApiPlatform/Resources/Store/BulkStoresSetStatus.php b/src/ApiPlatform/Resources/Store/BulkStoresSetStatus.php new file mode 100644 index 00000000..c5b7e7ad --- /dev/null +++ b/src/ApiPlatform/Resources/Store/BulkStoresSetStatus.php @@ -0,0 +1,62 @@ + + * @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\Store; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\Store\Command\BulkUpdateStoreStatusCommand; +use PrestaShop\PrestaShop\Core\Domain\Store\Exception\StoreNotFoundException; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSUpdate; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Validator\Constraints as Assert; + +#[ApiResource( + operations: [ + new CQRSUpdate( + uriTemplate: '/stores/set-status', + // No output 204 code + output: false, + CQRSCommand: BulkUpdateStoreStatusCommand::class, + CQRSCommandMapping: [ + '[enabled]' => '[expectedStatus]', + ], + scopes: [ + 'store_write', + ], + ), + ], + exceptionToStatus: [ + StoreNotFoundException::class => Response::HTTP_NOT_FOUND, + ], +)] +class BulkStoresSetStatus +{ + /** + * @var int[] + */ + #[ApiProperty(openapiContext: ['type' => 'array', 'items' => ['type' => 'integer'], 'example' => [1, 3]])] + #[Assert\NotBlank] + public array $storeIds; + + public bool $enabled; +} diff --git a/src/ApiPlatform/Resources/Store/Store.php b/src/ApiPlatform/Resources/Store/Store.php new file mode 100644 index 00000000..6ff8efac --- /dev/null +++ b/src/ApiPlatform/Resources/Store/Store.php @@ -0,0 +1,82 @@ + + * @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\Store; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\Store\Command\DeleteStoreCommand; +use PrestaShop\PrestaShop\Core\Domain\Store\Command\ToggleStoreStatusCommand; +use PrestaShop\PrestaShop\Core\Domain\Store\Exception\StoreNotFoundException; +use PrestaShop\PrestaShop\Core\Domain\Store\Query\GetStoreForEditing; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSDelete; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSUpdate; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Validator\Constraints as Assert; + +#[ApiResource( + operations: [ + new CQRSDelete( + uriTemplate: '/store/{storeId}', + requirements: ['storeId' => '\d+'], + output: false, + CQRSCommand: DeleteStoreCommand::class, + scopes: ['store_write'] + ), + new CQRSGet( + uriTemplate: '/store/{storeId}', + requirements: ['storeId' => '\d+'], + CQRSQuery: GetStoreForEditing::class, + scopes: ['store_read'], + CQRSQueryMapping: self::QUERY_MAPPING, + ), + new CQRSUpdate( + uriTemplate: '/store/{storeId}/toggle-status', + requirements: ['storeId' => '\d+'], + output: false, + allowEmptyBody: true, + CQRSCommand: ToggleStoreStatusCommand::class, + scopes: ['store_write'], + ), + ], + normalizationContext: ['skip_null_values' => false], + exceptionToStatus: [ + StoreNotFoundException::class => Response::HTTP_NOT_FOUND, + ], +)] +class Store +{ + #[ApiProperty(identifier: true)] + public int $storeId; + + #[Assert\NotNull(groups: ['Create'])] + public bool $enabled; + + public const COMMAND_MAPPING = [ + '[enabled]' => '[active]', + ]; + + public const QUERY_MAPPING = [ + '[active]' => '[enabled]', + ]; +} diff --git a/tests/Integration/ApiPlatform/StoreEndpointTest.php b/tests/Integration/ApiPlatform/StoreEndpointTest.php new file mode 100644 index 00000000..1bba2c2b --- /dev/null +++ b/tests/Integration/ApiPlatform/StoreEndpointTest.php @@ -0,0 +1,213 @@ + + * @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 Store; +use Symfony\Component\HttpFoundation\Response; +use Tests\Resources\DatabaseDump; + +class StoreEndpointTest extends ApiTestCase +{ + public static int $countryIdFR; + + public static \Store $store1; + + public static \Store $store2; + + public static \Store $store3; + + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + self::createApiClient(['store_read', 'store_write']); + + self::$countryIdFR = \Db::getInstance()->getValue('SELECT id_country FROM `' . _DB_PREFIX_ . 'country` WHERE iso_code="FR"'); + + // @todo : Replace it when the endpoint POST /store will be available. + self::$store1 = new \Store(); + self::$store1->name = [1 => 'Store 1']; + self::$store1->address1 = [1 => 'Store Address 1']; + self::$store1->address2 = [1 => 'Store Address 2']; + self::$store1->postcode = '50320'; + self::$store1->city = 'La Haye-Pesnel'; + self::$store1->latitude = '48.79652506'; + self::$store1->longitude = '-1.39708137'; + self::$store1->phone = '0233000000'; + self::$store1->email = 'store1@domain.tld'; + self::$store1->active = true; + self::$store1->id_country = self::$countryIdFR; + self::$store1->save(); + + self::$store2 = new \Store(); + self::$store2->name = [1 => 'Store 2']; + self::$store2->address1 = [1 => 'Store Address 3']; + self::$store2->address2 = [1 => 'Store Address 4']; + self::$store2->postcode = '62720'; + self::$store2->city = 'Rinxent'; + self::$store2->latitude = '50.80461760'; + self::$store2->longitude = '1.73905362'; + self::$store2->phone = '0321000000'; + self::$store2->email = 'store2@domain.tld'; + self::$store2->active = true; + self::$store2->id_country = self::$countryIdFR; + self::$store2->save(); + + self::$store3 = new \Store(); + self::$store3->name = [1 => 'Store 3']; + self::$store3->address1 = [1 => 'Store Address 5']; + self::$store3->address2 = [1 => 'Store Address 6']; + self::$store3->postcode = '35340'; + self::$store3->city = 'Liffre'; + self::$store3->latitude = '48.21403115'; + self::$store3->longitude = '-1.50542581'; + self::$store3->phone = '0321000000'; + self::$store3->email = 'store3@domain.tld'; + self::$store3->active = true; + self::$store3->id_country = self::$countryIdFR; + self::$store3->save(); + } + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + DatabaseDump::restoreTables(['store', 'store_lang', 'store_shop']); + } + + public static function getProtectedEndpoints(): iterable + { + yield 'get endpoint' => [ + 'GET', + '/store/1', + ]; + + yield 'toggle status endpoint' => [ + 'PUT', + '/store/1/toggle-status', + ]; + + yield 'delete endpoint' => [ + 'DELETE', + '/store/1', + ]; + + yield 'bulk set status endpoint' => [ + 'PUT', + '/stores/set-status', + ]; + + yield 'bulk delete endpoint' => [ + 'PUT', + '/stores/delete', + ]; + } + + public function testGetStore(): int + { + $storeId = (int) self::$store1->id; + + $store = $this->getItem('/store/' . $storeId, ['store_read']); + $this->assertEquals( + [ + 'storeId' => $storeId, + 'enabled' => true, + ], + $store + ); + + return $storeId; + } + + /** + * @depends testGetStore + */ + public function testToggleStatusStore(int $storeId): int + { + $this->updateItem('/store/' . $storeId . '/toggle-status', [], ['store_write'], Response::HTTP_NO_CONTENT); + $updatedStore = $this->getItem('/store/' . $storeId, ['store_read']); + $this->assertEquals( + [ + 'storeId' => $storeId, + 'enabled' => false, + ], + $updatedStore + ); + + return $storeId; + } + + /** + * @depends testToggleStatusStore + */ + public function testDeleteStore(int $storeId): void + { + $return = $this->deleteItem('/store/' . $storeId, ['store_write']); + // This endpoint return empty response and 204 HTTP code + $this->assertNull($return); + + // Getting the item should result in a 404 now + $this->getItem('/store/' . $storeId, ['store_read'], Response::HTTP_NOT_FOUND); + } + + /** + * @depends testDeleteStore + * + * @return array + */ + public function testBulkStatusStore(): array + { + $bulkStoresId = [ + self::$store2->id, + self::$store3->id, + ]; + + $this->updateItem('/stores/set-status', [ + 'storeIds' => $bulkStoresId, + 'enabled' => false, + ], ['store_write'], Response::HTTP_NO_CONTENT); + + foreach ($bulkStoresId as $storeId) { + $store = $this->getItem('/store/' . $storeId, ['store_read']); + $this->assertEquals(false, $store['enabled']); + } + + return $bulkStoresId; + } + + /** + * @depends testBulkStatusStore + * + * @param array $bulkStoresId + */ + public function testBulkDeleteStore(array $bulkStoresId): void + { + $this->updateItem('/stores/delete', [ + 'storeIds' => $bulkStoresId, + ], ['store_write'], Response::HTTP_NO_CONTENT); + + // Assert the provided stores have been removed + foreach ($bulkStoresId as $storeId) { + $this->getItem('/stores/' . $storeId, ['store_read'], Response::HTTP_NOT_FOUND); + } + } +} diff --git a/tests/Integration/ApiPlatform/SupplierEndpointTest.php b/tests/Integration/ApiPlatform/SupplierEndpointTest.php index 77d6b1c9..bd041b0f 100644 --- a/tests/Integration/ApiPlatform/SupplierEndpointTest.php +++ b/tests/Integration/ApiPlatform/SupplierEndpointTest.php @@ -84,12 +84,11 @@ public static function getProtectedEndpoints(): iterable 'GET', '/suppliers', ]; - /* - yield 'bulk delete endpoint' => [ - 'PUT', - '/suppliers/delete', - ]; - */ + + yield 'bulk delete endpoint' => [ + 'PUT', + '/suppliers/delete', + ]; yield 'bulk disable endpoint' => [ 'PUT',