From dead3ac6cd0d49b79b58e3d34e865d6cffde0272 Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Wed, 11 Oct 2023 14:04:27 +0200 Subject: [PATCH 01/14] [API][Admin] Extend GET endpoints for getting product variants --- .../config/serialization/ProductVariant.xml | 43 +- .../ProductVariantTranslation.xml | 15 +- tests/Api/Admin/ProductVariantsTest.php | 423 ++++++++++-------- .../ORM/product/product_variant.yaml | 72 ++- .../get_product_variant_response.json | 38 +- .../get_product_variants_response.json | 65 ++- ...ost_product_variant_disabled_response.json | 27 +- .../post_product_variant_response.json | 38 +- .../put_product_variant_response.json | 27 +- 9 files changed, 511 insertions(+), 237 deletions(-) diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml index 2666a4aba0..c0d639ba59 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml @@ -27,8 +27,8 @@ shop:product_variant:read - admin:product_variant:create admin:product_variant:read + admin:product_variant:create admin:product_variant:update @@ -36,8 +36,8 @@ shop:product_variant:read - admin:product_variant:create admin:product_variant:read + admin:product_variant:create admin:product_variant:update @@ -48,5 +48,44 @@ admin:product_variant:create admin:product_variant:update + + admin:product_variant:read + + + admin:product_variant:read + + + admin:product_variant:read + + + admin:product_variant:read + + + admin:product_variant:read + + + admin:product_variant:read + + + admin:product_variant:read + + + admin:product_variant:read + + + admin:product_variant:read + + + admin:product_variant:read + + + admin:product_variant:read + + + admin:product_variant:read + + + admin:product_variant:read + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariantTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariantTranslation.xml index 7d726be1c1..0106d3bde8 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariantTranslation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariantTranslation.xml @@ -17,25 +17,26 @@ > - shop:order:account:read + admin:product_variant:read admin:order:read shop:product_variant:read + shop:order:account:read - shop:order:account:read - admin:order:read + admin:product_variant:read admin:product_variant:create admin:product_variant:update - admin:product_variant:read + admin:order:read shop:product_variant:read + shop:order:account:read - shop:order:account:read - admin:order:read + admin:product_variant:read admin:product_variant:create admin:product_variant:update - admin:product_variant:read + admin:order:read shop:product_variant:read + shop:order:account:read diff --git a/tests/Api/Admin/ProductVariantsTest.php b/tests/Api/Admin/ProductVariantsTest.php index 823a799e05..d2e72b7d32 100644 --- a/tests/Api/Admin/ProductVariantsTest.php +++ b/tests/Api/Admin/ProductVariantsTest.php @@ -14,7 +14,6 @@ declare(strict_types=1); namespace Sylius\Tests\Api\Admin; use Sylius\Component\Core\Model\ChannelInterface; -use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Tests\Api\JsonApiTestCase; use Sylius\Tests\Api\Utils\AdminUserLoginTrait; @@ -25,9 +24,14 @@ final class ProductVariantsTest extends JsonApiTestCase use AdminUserLoginTrait; /** @test */ - public function it_denies_access_to_a_products_list_for_not_authenticated_user(): void + public function it_denies_access_to_a_product_variants_list_for_not_authenticated_user(): void { - $this->loadFixturesFromFiles(['channel.yaml', 'product/product_variant.yaml']); + $this->loadFixturesFromFiles([ + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); $this->client->request('GET', '/api/v2/admin/product-variants'); @@ -38,7 +42,13 @@ final class ProductVariantsTest extends JsonApiTestCase /** @test */ public function it_gets_all_product_variants(): void { - $this->loadFixturesFromFiles(['channel.yaml', 'product/product_variant.yaml', 'authentication/api_administrator.yaml']); + $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); $this->client->request( @@ -54,7 +64,13 @@ final class ProductVariantsTest extends JsonApiTestCase /** @test */ public function it_gets_a_product_variant(): void { - $fixtures = $this->loadFixturesFromFiles(['channel.yaml', 'product/product_variant.yaml', 'authentication/api_administrator.yaml']); + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); /** @var ProductVariantInterface $productVariant */ @@ -70,10 +86,217 @@ final class ProductVariantsTest extends JsonApiTestCase $this->assertResponse($response, 'admin/product_variant/get_product_variant_response', Response::HTTP_OK); } + /** @test */ + public function it_creates_product_variant_enabled_by_default(): void + { + $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + $this->client->request( + method: 'POST', + uri: '/api/v2/admin/product-variants', + server: $header, + content: json_encode([ + 'code' => 'MUG_3', + 'position' => 1, + 'product' => '/api/v2/admin/products/MUG_SW', + 'channelPricings' => ['WEB' => [ + 'channelCode' => 'WEB', + 'price' => 4000, + 'originalPrice' => 5000, + 'minimumPrice' => 2000, + ]], + 'translations' => [ + 'en_US' => [ + 'locale' => 'en_US', + 'name' => 'Yellow mug', + ], + ], + ], JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/product_variant/post_product_variant_response', + Response::HTTP_CREATED, + ); + } + + /** @test */ + public function it_creates_disabled_product_variant(): void + { + $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + $this->client->request( + method: 'POST', + uri: '/api/v2/admin/product-variants', + server: $header, + content: json_encode([ + 'code' => 'MUG_3', + 'position' => 1, + 'product' => '/api/v2/admin/products/MUG_SW', + 'channelPricings' => ['WEB' => [ + 'channelCode' => 'WEB', + 'price' => 4000, + 'originalPrice' => 5000, + 'minimumPrice' => 2000, + ]], + 'enabled' => false, + ], JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/product_variant/post_product_variant_disabled_response', + Response::HTTP_CREATED, + ); + } + + /** @test */ + public function it_does_not_allow_to_create_product_variant_with_invalid_channel_code(): void + { + $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + $this->client->request( + method: 'POST', + uri: '/api/v2/admin/product-variants', + server: $header, + content: json_encode([ + 'code' => 'CUP', + 'product' => '/api/v2/admin/products/MUG_SW', + 'channelPricings' => ['NON-EXISTING-CHANNEL' => [ + 'channelCode' => 'NON-EXISTING-CHANNEL', + 'price' => 4000, + ]], + ], JSON_THROW_ON_ERROR), + ); + + $this->assertResponseCode($this->client->getResponse(), Response::HTTP_UNPROCESSABLE_ENTITY); + } + + /** @test */ + public function it_does_not_allow_to_create_product_variant_without_channel_code(): void + { + $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + $this->client->request( + method: 'POST', + uri: '/api/v2/admin/product-variants', + server: $header, + content: json_encode([ + 'code' => 'CUP', + 'product' => '/api/v2/admin/products/MUG_SW', + 'channelPricings' => [ + 'NON-EXISTING-CHANNEL' => ['price' => 4000] + ], + ], JSON_THROW_ON_ERROR), + ); + + $this->assertResponseCode($this->client->getResponse(), Response::HTTP_UNPROCESSABLE_ENTITY); + } + + /** @test */ + public function it_does_not_allow_to_create_product_variant_without_product(): void + { + $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + $this->client->request( + method: 'POST', + uri: '/api/v2/admin/product-variants', + server: $header, + content: json_encode([ + 'code' => 'CUP', + 'channelPricings' => ['WEB' => [ + 'channelCode' => 'WEB', + 'price' => 4000, + ]], + ], JSON_THROW_ON_ERROR), + ); + + $this->assertResponseCode($this->client->getResponse(), Response::HTTP_UNPROCESSABLE_ENTITY); + } + + /** @test */ + public function it_does_not_allow_to_create_product_variant_with_invalid_locale_code(): void + { + $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + $this->client->request( + method: 'POST', + uri: '/api/v2/admin/product-variants', + server: $header, + content: json_encode([ + 'code' => 'CUP', + 'product' => '/api/v2/admin/products/MUG_SW', + 'channelPricings' => ['WEB' => [ + 'channelCode' => 'WEB', + 'price' => 4000, + 'originalPrice' => 5000, + 'minimumPrice' => 2000, + ]], + 'translations' => [ + 'NON-EXISTING-LOCALE-CODE' => [ + 'locale' => 'NON-EXISTING-LOCALE-CODE', + 'name' => 'Yellow mug', + ], + ], + ], JSON_THROW_ON_ERROR), + ); + + $this->assertResponseCode($this->client->getResponse(), Response::HTTP_UNPROCESSABLE_ENTITY); + } + /** @test */ public function it_updates_channel_pricing_and_translation_of_a_product_variant(): void { - $fixtures = $this->loadFixturesFromFiles(['channel.yaml', 'product/product_variant.yaml', 'authentication/api_administrator.yaml']); + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); /** @var ProductVariantInterface $productVariant */ @@ -113,190 +336,16 @@ final class ProductVariantsTest extends JsonApiTestCase ); } - /** @test */ - public function it_creates_product_variant_enabled_by_default(): void - { - $fixtures = $this->loadFixturesFromFiles(['channel.yaml', 'product/product_variant.yaml', 'authentication/api_administrator.yaml']); - $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); - - /** @var ProductInterface $product */ - $product = $fixtures['product']; - - $this->client->request( - method: 'POST', - uri: '/api/v2/admin/product-variants', - server: $header, - content: json_encode([ - 'code' => 'MUG_2', - 'position' => 1, - 'product' => sprintf('/api/v2/admin/products/%s', $product->getCode()), - 'channelPricings' => ['WEB' => [ - 'channelCode' => 'WEB', - 'price' => 4000, - 'originalPrice' => 5000, - 'minimumPrice' => 2000, - ]], - 'translations' => [ - 'en_US' => [ - 'locale' => 'en_US', - 'name' => 'Yellow mug', - ], - ], - ], JSON_THROW_ON_ERROR), - ); - - $this->assertResponse( - $this->client->getResponse(), - 'admin/product_variant/post_product_variant_response', - Response::HTTP_CREATED, - ); - } - - /** @test */ - public function it_creates_disabled_product_variant(): void - { - $fixtures = $this->loadFixturesFromFiles(['channel.yaml', 'product/product_variant.yaml', 'authentication/api_administrator.yaml']); - $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); - - /** @var ProductInterface $product */ - $product = $fixtures['product']; - - $this->client->request( - method: 'POST', - uri: '/api/v2/admin/product-variants', - server: $header, - content: json_encode([ - 'code' => 'MUG_2', - 'position' => 1, - 'product' => sprintf('/api/v2/admin/products/%s', $product->getCode()), - 'channelPricings' => ['WEB' => [ - 'channelCode' => 'WEB', - 'price' => 4000, - 'originalPrice' => 5000, - 'minimumPrice' => 2000, - ]], - 'enabled' => false, - ], JSON_THROW_ON_ERROR), - ); - - $this->assertResponse( - $this->client->getResponse(), - 'admin/product_variant/post_product_variant_disabled_response', - Response::HTTP_CREATED, - ); - } - - /** @test */ - public function it_does_not_allow_to_create_product_variant_with_invalid_channel_code(): void - { - $fixtures = $this->loadFixturesFromFiles(['channel.yaml', 'product/product_variant.yaml', 'authentication/api_administrator.yaml']); - $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); - - /** @var ProductInterface $product */ - $product = $fixtures['product']; - - $this->client->request( - method: 'POST', - uri: '/api/v2/admin/product-variants', - server: $header, - content: json_encode([ - 'code' => 'CUP', - 'product' => sprintf('/api/v2/admin/products/%s', $product->getCode()), - 'channelPricings' => ['NON-EXISTING-CHANNEL' => [ - 'channelCode' => 'NON-EXISTING-CHANNEL', - 'price' => 4000, - ]], - ], JSON_THROW_ON_ERROR), - ); - - $this->assertResponseCode($this->client->getResponse(), Response::HTTP_UNPROCESSABLE_ENTITY); - } - - /** @test */ - public function it_does_not_allow_to_create_product_variant_without_channel_code(): void - { - $fixtures = $this->loadFixturesFromFiles(['channel.yaml', 'product/product_variant.yaml', 'authentication/api_administrator.yaml']); - $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); - - /** @var ProductInterface $product */ - $product = $fixtures['product']; - - $this->client->request( - method: 'POST', - uri: '/api/v2/admin/product-variants', - server: $header, - content: json_encode([ - 'code' => 'CUP', - 'product' => sprintf('/api/v2/admin/products/%s', $product->getCode()), - 'channelPricings' => [ - 'NON-EXISTING-CHANNEL' => ['price' => 4000] - ], - ], JSON_THROW_ON_ERROR), - ); - - $this->assertResponseCode($this->client->getResponse(), Response::HTTP_UNPROCESSABLE_ENTITY); - } - - /** @test */ - public function it_does_not_allow_to_create_product_variant_without_product(): void - { - $this->loadFixturesFromFiles(['channel.yaml', 'product/product_variant.yaml', 'authentication/api_administrator.yaml']); - $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); - - $this->client->request( - method: 'POST', - uri: '/api/v2/admin/product-variants', - server: $header, - content: json_encode([ - 'code' => 'CUP', - 'channelPricings' => ['WEB' => [ - 'channelCode' => 'WEB', - 'price' => 4000, - ]], - ], JSON_THROW_ON_ERROR), - ); - - $this->assertResponseCode($this->client->getResponse(), Response::HTTP_UNPROCESSABLE_ENTITY); - } - - /** @test */ - public function it_does_not_allow_to_create_product_variant_with_invalid_locale_code(): void - { - $fixtures = $this->loadFixturesFromFiles(['channel.yaml', 'product/product_variant.yaml', 'authentication/api_administrator.yaml']); - $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); - - /** @var ProductInterface $product */ - $product = $fixtures['product']; - - $this->client->request( - method: 'POST', - uri: '/api/v2/admin/product-variants', - server: $header, - content: json_encode([ - 'code' => 'CUP', - 'product' => sprintf('/api/v2/admin/products/%s', $product->getCode()), - 'channelPricings' => ['WEB' => [ - 'channelCode' => 'WEB', - 'price' => 4000, - 'originalPrice' => 5000, - 'minimumPrice' => 2000, - ]], - 'translations' => [ - 'NON-EXISTING-LOCALE-CODE' => [ - 'locale' => 'NON-EXISTING-LOCALE-CODE', - 'name' => 'Yellow mug', - ], - ], - ], JSON_THROW_ON_ERROR), - ); - - $this->assertResponseCode($this->client->getResponse(), Response::HTTP_UNPROCESSABLE_ENTITY); - } - /** @test */ public function it_does_not_allow_to_update_product_variant_with_invalid_locale_code(): void { - $fixtures = $this->loadFixturesFromFiles(['channel.yaml', 'product/product_variant.yaml', 'authentication/api_administrator.yaml']); + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); /** @var ProductVariantInterface $productVariant */ diff --git a/tests/Api/DataFixtures/ORM/product/product_variant.yaml b/tests/Api/DataFixtures/ORM/product/product_variant.yaml index 13764eb523..08fc01d327 100644 --- a/tests/Api/DataFixtures/ORM/product/product_variant.yaml +++ b/tests/Api/DataFixtures/ORM/product/product_variant.yaml @@ -2,28 +2,96 @@ Sylius\Component\Core\Model\Product: product: fallbackLocale: en_US currentLocale: en - code: MUG_SW + code: 'MUG_SW' Sylius\Component\Product\Model\ProductVariantTranslation: product_variant_translation: name: 'Mug' locale: en_US translatable: '@product_variant' + product_variant_2_translation: + name: 'Mug 2' + locale: en_US + translatable: '@product_variant_2' Sylius\Component\Core\Model\ProductVariant: product_variant: - code: MUG + code: 'MUG' version: 1 product: '@product' fallbackLocale: en_US currentLocale: en position: 1 + optionValues: ['@product_option_value_color_blue'] channelPricings: channel_web: '@product_variant_channel_web_pricing' translations: - '@product_variant_translation' + enabled: true + tracked: true + onHold: 0 + onHand: 10 + weight: 100.50 + width: 100.50 + height: 100.50 + depth: 100.50 + taxCategory: '@tax_category_default' + shippingCategory: '@shipping_category_default' + shippingRequired: true + product_variant_2: + code: 'MUG_2' + product: '@product' + fallbackLocale: en_US + currentLocale: en + position: 2 + channelPricings: + channel_web: '@product_variant_2_channel_web_pricing' + translations: + - '@product_variant_2_translation' Sylius\Component\Core\Model\ChannelPricing: product_variant_channel_web_pricing: channelCode: 'WEB' price: 2000 + product_variant_2_channel_web_pricing: + channelCode: 'WEB' + price: 3000 + +Sylius\Component\Product\Model\ProductOption: + product_option_color: + code: 'COLOR' + currentLocale: 'en_US' + translations: + - '@product_option_translation_en_EN' + +Sylius\Component\Product\Model\ProductOptionTranslation: + product_option_translation_en_EN: + locale: 'en_US' + name: 'Color' + translatable: '@product_option_color' + +Sylius\Component\Product\Model\ProductOptionValue: + product_option_value_color_blue: + code: 'COLOR_BLUE' + currentLocale: 'en_US' + fallbackLocale: 'en_US' + option: '@product_option_color' + translations: + - '@product_option_value_translation_blue' + product_option_value_color_red: + code: 'COLOR_RED' + currentLocale: 'en_US' + fallbackLocale: 'en_US' + option: '@product_option_color' + translations: + - '@product_option_value_translation_red' + +Sylius\Component\Product\Model\ProductOptionValueTranslation: + product_option_value_translation_blue: + locale: 'en_US' + value: 'Blue' + translatable: '@product_option_value_color_blue' + product_option_value_translation_red: + locale: 'en_US' + value: 'Red' + translatable: '@product_option_value_color_red' diff --git a/tests/Api/Responses/Expected/admin/product_variant/get_product_variant_response.json b/tests/Api/Responses/Expected/admin/product_variant/get_product_variant_response.json index 810c8234bd..bd4f0c6e9c 100644 --- a/tests/Api/Responses/Expected/admin/product_variant/get_product_variant_response.json +++ b/tests/Api/Responses/Expected/admin/product_variant/get_product_variant_response.json @@ -1,9 +1,12 @@ { - "@context":"\/api\/v2\/contexts\/ProductVariant", - "@id":"\/api\/v2\/admin\/product-variants\/MUG", - "@type":"ProductVariant", - "code":"MUG", - "product":"\/api\/v2\/admin\/products\/MUG_SW", + "@context": "\/api\/v2\/contexts\/ProductVariant", + "@id": "\/api\/v2\/admin\/product-variants\/MUG", + "@type": "ProductVariant", + "code": "MUG", + "product": "\/api\/v2\/admin\/products\/MUG_SW", + "optionValues":[ + "\/api\/v2\/admin\/product-option-values\/COLOR_BLUE" + ], "channelPricings": { "WEB": { "@id": "/api/v2/admin/channel-pricings/@integer@", @@ -15,14 +18,27 @@ "minimumPrice": 0 } }, - "optionValues":[], "translations": { "en_US": { - "@id":"\/api\/v2\/admin\/product-variant-translations\/@integer@", - "@type":"ProductVariantTranslation", - "name":"Mug", - "locale":"en_US" + "@id": "\/api\/v2\/admin\/product-variant-translations\/@integer@", + "@type": "ProductVariantTranslation", + "id": @integer@, + "name": "Mug", + "locale": "en_US" } }, - "enabled": true + "enabled": true, + "position": 1, + "tracked": true, + "onHold": 0, + "onHand": 10, + "weight": 100.5, + "width": 100.5, + "height": 100.5, + "depth": 100.5, + "taxCategory": "\/api\/v2\/admin\/tax-categories\/default", + "shippingCategory": "\/api\/v2\/admin\/shipping-categories\/default", + "shippingRequired": true, + "createdAt": @date@, + "updatedAt": @date@ } diff --git a/tests/Api/Responses/Expected/admin/product_variant/get_product_variants_response.json b/tests/Api/Responses/Expected/admin/product_variant/get_product_variants_response.json index d745922689..8b55d7452f 100644 --- a/tests/Api/Responses/Expected/admin/product_variant/get_product_variants_response.json +++ b/tests/Api/Responses/Expected/admin/product_variant/get_product_variants_response.json @@ -8,6 +8,9 @@ "@type": "ProductVariant", "code": "MUG", "product": "\/api\/v2\/admin\/products\/MUG_SW", + "optionValues":[ + "\/api\/v2\/admin\/product-option-values\/COLOR_BLUE" + ], "channelPricings": { "WEB": { "@id": "/api/v2/admin/channel-pricings/@integer@", @@ -19,19 +22,73 @@ "minimumPrice": 0 } }, - "optionValues": [], - "enabled": true, "translations": { "en_US": { "@id": "\/api\/v2\/admin\/product-variant-translations\/@integer@", "@type": "ProductVariantTranslation", + "id": @integer@, "name": "Mug", "locale": "en_US" } - } + }, + "enabled": true, + "position": 1, + "tracked": true, + "onHold": 0, + "onHand": 10, + "weight": 100.5, + "width": 100.5, + "height": 100.5, + "depth": 100.5, + "taxCategory": "\/api\/v2\/admin\/tax-categories\/default", + "shippingCategory": "\/api\/v2\/admin\/shipping-categories\/default", + "shippingRequired": true, + "createdAt": @date@, + "updatedAt": @date@ + }, + { + "@id": "\/api\/v2\/admin\/product-variants/MUG_2", + "@type": "ProductVariant", + "code": "MUG_2", + "product": "\/api\/v2\/admin\/products\/MUG_SW", + "optionValues": [], + "channelPricings": { + "WEB": { + "@id": "/api/v2/admin/channel-pricings/@integer@", + "@type": "ChannelPricing", + "channelCode": "WEB", + "price": 3000, + "originalPrice": null, + "lowestPriceBeforeDiscount": null, + "minimumPrice": 0 + } + }, + "translations": { + "en_US": { + "@id": "\/api\/v2\/admin\/product-variant-translations\/@integer@", + "@type": "ProductVariantTranslation", + "id": @integer@, + "name": "Mug 2", + "locale": "en_US" + } + }, + "enabled": true, + "position": 2, + "tracked": false, + "onHold": 0, + "onHand": 0, + "weight": null, + "width": null, + "height": null, + "depth": null, + "taxCategory": null, + "shippingCategory": null, + "shippingRequired": true, + "createdAt": @date@, + "updatedAt": @date@ } ], - "hydra:totalItems": 1, + "hydra:totalItems": 2, "hydra:search": { "@type": "hydra:IriTemplate", "hydra:template": "\/api\/v2\/admin\/product-variants{?catalogPromotion}", diff --git a/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_disabled_response.json b/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_disabled_response.json index 7eb704b1a9..0e603b42c3 100644 --- a/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_disabled_response.json +++ b/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_disabled_response.json @@ -1,9 +1,10 @@ { - "@context":"\/api\/v2\/contexts\/ProductVariant", - "@id":"\/api\/v2\/admin\/product-variants\/MUG_2", - "@type":"ProductVariant", - "code":"MUG_2", - "product":"\/api\/v2\/admin\/products\/MUG_SW", + "@context": "\/api\/v2\/contexts\/ProductVariant", + "@id": "\/api\/v2\/admin\/product-variants\/MUG_3", + "@type": "ProductVariant", + "code": "MUG_3", + "product": "\/api\/v2\/admin\/products\/MUG_SW", + "optionValues": [], "channelPricings": { "WEB": { "@id": "\/api\/v2\/admin\/channel-pricings\/@integer@", @@ -15,7 +16,19 @@ "lowestPriceBeforeDiscount": null } }, + "translations": [], "enabled": false, - "optionValues": [], - "translations": [] + "position": 3, + "tracked": false, + "onHold": 0, + "onHand": 0, + "weight": null, + "width": null, + "height": null, + "depth": null, + "taxCategory": null, + "shippingCategory": null, + "shippingRequired": true, + "createdAt": @date@, + "updatedAt": @date@ } diff --git a/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_response.json b/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_response.json index 58b1fc8758..bab20563fb 100644 --- a/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_response.json +++ b/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_response.json @@ -1,9 +1,10 @@ { - "@context":"\/api\/v2\/contexts\/ProductVariant", - "@id":"\/api\/v2\/admin\/product-variants\/MUG_2", - "@type":"ProductVariant", - "code":"MUG_2", - "product":"\/api\/v2\/admin\/products\/MUG_SW", + "@context": "\/api\/v2\/contexts\/ProductVariant", + "@id": "\/api\/v2\/admin\/product-variants\/MUG_3", + "@type": "ProductVariant", + "code": "MUG_3", + "product": "\/api\/v2\/admin\/products\/MUG_SW", + "optionValues": [], "channelPricings": { "WEB": { "@id": "\/api\/v2\/admin\/channel-pricings\/@integer@", @@ -15,14 +16,27 @@ "lowestPriceBeforeDiscount": null } }, - "enabled": true, - "optionValues": [], "translations": { "en_US": { - "@id":"\/api\/v2\/admin\/product-variant-translations\/@integer@", - "@type":"ProductVariantTranslation", - "name":"Yellow mug", - "locale":"en_US" + "@id": "\/api\/v2\/admin\/product-variant-translations\/@integer@", + "@type": "ProductVariantTranslation", + "id":@integer@, + "name": "Yellow mug", + "locale": "en_US" } - } + }, + "enabled": true, + "position": 3, + "tracked": false, + "onHold": 0, + "onHand": 0, + "weight": null, + "width": null, + "height": null, + "depth": null, + "taxCategory": null, + "shippingCategory": null, + "shippingRequired": true, + "createdAt": @date@, + "updatedAt": @date@ } diff --git a/tests/Api/Responses/Expected/admin/product_variant/put_product_variant_response.json b/tests/Api/Responses/Expected/admin/product_variant/put_product_variant_response.json index 51823bcb5b..debd39034a 100644 --- a/tests/Api/Responses/Expected/admin/product_variant/put_product_variant_response.json +++ b/tests/Api/Responses/Expected/admin/product_variant/put_product_variant_response.json @@ -2,6 +2,11 @@ "@context": "\/api\/v2\/contexts\/ProductVariant", "@id": "\/api\/v2\/admin\/product-variants\/MUG", "@type": "ProductVariant", + "code": "MUG", + "product": "\/api\/v2\/admin\/products\/MUG_SW", + "optionValues":[ + "\/api\/v2\/admin\/product-option-values\/COLOR_BLUE" + ], "channelPricings": { "WEB": { "@id": "\/api\/v2\/admin\/channel-pricings\/@integer@", @@ -13,22 +18,34 @@ "minimumPrice": 210 } }, - "code": "MUG", - "product": "\/api\/v2\/admin\/products\/MUG_SW", - "optionValues": [], - "enabled": true, "translations": { "de_DE": { "@id": "\/api\/v2\/admin\/product-variant-translations\/@integer@", "@type": "ProductVariantTranslation", + "id": @integer@, "name": "Orange Tasse", "locale": "de_DE" }, "pl_PL": { "@id": "\/api\/v2\/admin\/product-variant-translations\/@integer@", "@type": "ProductVariantTranslation", + "id": @integer@, "name": "Pomarańczowy kubek", "locale": "pl_PL" } - } + }, + "enabled": true, + "position": 1, + "tracked": true, + "onHold": 0, + "onHand": 10, + "weight": 100.5, + "width": 100.5, + "height": 100.5, + "depth": 100.5, + "taxCategory": "\/api\/v2\/admin\/tax-categories\/default", + "shippingCategory": "\/api\/v2\/admin\/shipping-categories\/default", + "shippingRequired": true, + "createdAt": @date@, + "updatedAt": @date@ } From 82f1e7762e06a0a118219cd9c58e64db461d34e2 Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Wed, 11 Oct 2023 14:58:36 +0200 Subject: [PATCH 02/14] [API][Admin] Extend POST endpoint for creating a product variant --- .../config/serialization/ProductVariant.xml | 12 +++++++ tests/Api/Admin/ProductVariantsTest.php | 28 +++++++++------ ..._variant_enabled_by_default_response.json} | 6 ++-- .../post_product_variant_response.json | 34 ++++++++++--------- 4 files changed, 51 insertions(+), 29 deletions(-) rename tests/Api/Responses/Expected/admin/product_variant/{post_product_variant_disabled_response.json => post_product_variant_enabled_by_default_response.json} (90%) diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml index c0d639ba59..276e6d4254 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml @@ -33,6 +33,7 @@ admin:product_variant:read + admin:product_variant:create shop:product_variant:read @@ -50,36 +51,47 @@ admin:product_variant:read + admin:product_variant:create admin:product_variant:read + admin:product_variant:create admin:product_variant:read + admin:product_variant:create admin:product_variant:read + admin:product_variant:create admin:product_variant:read + admin:product_variant:create admin:product_variant:read + admin:product_variant:create admin:product_variant:read + admin:product_variant:create admin:product_variant:read + admin:product_variant:create admin:product_variant:read + admin:product_variant:create admin:product_variant:read + admin:product_variant:create admin:product_variant:read + admin:product_variant:create admin:product_variant:read diff --git a/tests/Api/Admin/ProductVariantsTest.php b/tests/Api/Admin/ProductVariantsTest.php index d2e72b7d32..51cbb52c56 100644 --- a/tests/Api/Admin/ProductVariantsTest.php +++ b/tests/Api/Admin/ProductVariantsTest.php @@ -87,7 +87,7 @@ final class ProductVariantsTest extends JsonApiTestCase } /** @test */ - public function it_creates_product_variant_enabled_by_default(): void + public function it_creates_a_product_variant_with_all_optional_data(): void { $this->loadFixturesFromFiles([ 'authentication/api_administrator.yaml', @@ -103,9 +103,9 @@ final class ProductVariantsTest extends JsonApiTestCase uri: '/api/v2/admin/product-variants', server: $header, content: json_encode([ - 'code' => 'MUG_3', - 'position' => 1, + 'code' => 'MUG_RED', 'product' => '/api/v2/admin/products/MUG_SW', + 'optionValues' => ['/api/v2/admin/product-option-values/COLOR_RED'], 'channelPricings' => ['WEB' => [ 'channelCode' => 'WEB', 'price' => 4000, @@ -115,9 +115,21 @@ final class ProductVariantsTest extends JsonApiTestCase 'translations' => [ 'en_US' => [ 'locale' => 'en_US', - 'name' => 'Yellow mug', + 'name' => 'Red mug', ], ], + 'enabled' => false, + 'position' => 1, + 'tracked' => true, + 'onHold' => 5, + 'onHand' => 10, + 'weight' => 100.5, + 'width' => 100.5, + 'height' => 100.5, + 'depth' => 100.5, + 'taxCategory' => '/api/v2/admin/tax-categories/default', + 'shippingCategory' => '/api/v2/admin/shipping-categories/default', + 'shippingRequired' => true, ], JSON_THROW_ON_ERROR), ); @@ -129,7 +141,7 @@ final class ProductVariantsTest extends JsonApiTestCase } /** @test */ - public function it_creates_disabled_product_variant(): void + public function it_creates_a_product_variant_enabled_by_default(): void { $this->loadFixturesFromFiles([ 'authentication/api_administrator.yaml', @@ -146,21 +158,17 @@ final class ProductVariantsTest extends JsonApiTestCase server: $header, content: json_encode([ 'code' => 'MUG_3', - 'position' => 1, 'product' => '/api/v2/admin/products/MUG_SW', 'channelPricings' => ['WEB' => [ 'channelCode' => 'WEB', 'price' => 4000, - 'originalPrice' => 5000, - 'minimumPrice' => 2000, ]], - 'enabled' => false, ], JSON_THROW_ON_ERROR), ); $this->assertResponse( $this->client->getResponse(), - 'admin/product_variant/post_product_variant_disabled_response', + 'admin/product_variant/post_product_variant_enabled_by_default_response', Response::HTTP_CREATED, ); } diff --git a/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_disabled_response.json b/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_enabled_by_default_response.json similarity index 90% rename from tests/Api/Responses/Expected/admin/product_variant/post_product_variant_disabled_response.json rename to tests/Api/Responses/Expected/admin/product_variant/post_product_variant_enabled_by_default_response.json index 0e603b42c3..ea7a657497 100644 --- a/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_disabled_response.json +++ b/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_enabled_by_default_response.json @@ -11,13 +11,13 @@ "@type": "ChannelPricing", "channelCode": "WEB", "price": 4000, - "originalPrice": 5000, - "minimumPrice": 2000, + "originalPrice": null, + "minimumPrice": 0, "lowestPriceBeforeDiscount": null } }, "translations": [], - "enabled": false, + "enabled": true, "position": 3, "tracked": false, "onHold": 0, diff --git a/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_response.json b/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_response.json index bab20563fb..c40a16c5f2 100644 --- a/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_response.json +++ b/tests/Api/Responses/Expected/admin/product_variant/post_product_variant_response.json @@ -1,10 +1,12 @@ { "@context": "\/api\/v2\/contexts\/ProductVariant", - "@id": "\/api\/v2\/admin\/product-variants\/MUG_3", + "@id": "\/api\/v2\/admin\/product-variants\/MUG_RED", "@type": "ProductVariant", - "code": "MUG_3", + "code": "MUG_RED", "product": "\/api\/v2\/admin\/products\/MUG_SW", - "optionValues": [], + "optionValues": [ + "\/api\/v2\/admin\/product-option-values\/COLOR_RED" + ], "channelPricings": { "WEB": { "@id": "\/api\/v2\/admin\/channel-pricings\/@integer@", @@ -20,22 +22,22 @@ "en_US": { "@id": "\/api\/v2\/admin\/product-variant-translations\/@integer@", "@type": "ProductVariantTranslation", - "id":@integer@, - "name": "Yellow mug", + "id": @integer@, + "name": "Red mug", "locale": "en_US" } }, - "enabled": true, - "position": 3, - "tracked": false, - "onHold": 0, - "onHand": 0, - "weight": null, - "width": null, - "height": null, - "depth": null, - "taxCategory": null, - "shippingCategory": null, + "enabled": false, + "position": 1, + "tracked": true, + "onHold": 5, + "onHand": 10, + "weight": 100.5, + "width": 100.5, + "height": 100.5, + "depth": 100.5, + "taxCategory": "\/api\/v2\/admin\/tax-categories\/default", + "shippingCategory": "\/api\/v2\/admin\/shipping-categories\/default", "shippingRequired": true, "createdAt": @date@, "updatedAt": @date@ From 1bbfc60b7d39599d124543b1ed0e566dd1a9c918 Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Wed, 11 Oct 2023 15:17:12 +0200 Subject: [PATCH 03/14] [API][Admin] Extend PUT endpoint for updating the product variant --- .../config/serialization/ChannelPricing.xml | 6 ++-- .../config/serialization/ProductVariant.xml | 12 ++++++++ tests/Api/Admin/ProductVariantsTest.php | 23 ++++++++++---- .../put_product_variant_response.json | 30 +++++++++---------- 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ChannelPricing.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ChannelPricing.xml index 1ec60c4d4d..c233150bf8 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ChannelPricing.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ChannelPricing.xml @@ -23,20 +23,20 @@ admin:product_variant:read - admin:product_variant:update admin:product_variant:create + admin:product_variant:update admin:product_variant:read - admin:product_variant:update admin:product_variant:create + admin:product_variant:update admin:product_variant:read - admin:product_variant:update admin:product_variant:create + admin:product_variant:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml index 276e6d4254..0325d74aee 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml @@ -34,6 +34,7 @@ admin:product_variant:read admin:product_variant:create + admin:product_variant:update shop:product_variant:read @@ -52,46 +53,57 @@ admin:product_variant:read admin:product_variant:create + admin:product_variant:update admin:product_variant:read admin:product_variant:create + admin:product_variant:update admin:product_variant:read admin:product_variant:create + admin:product_variant:update admin:product_variant:read admin:product_variant:create + admin:product_variant:update admin:product_variant:read admin:product_variant:create + admin:product_variant:update admin:product_variant:read admin:product_variant:create + admin:product_variant:update admin:product_variant:read admin:product_variant:create + admin:product_variant:update admin:product_variant:read admin:product_variant:create + admin:product_variant:update admin:product_variant:read admin:product_variant:create + admin:product_variant:update admin:product_variant:read admin:product_variant:create + admin:product_variant:update admin:product_variant:read admin:product_variant:create + admin:product_variant:update admin:product_variant:read diff --git a/tests/Api/Admin/ProductVariantsTest.php b/tests/Api/Admin/ProductVariantsTest.php index 51cbb52c56..8c7b1ea3e2 100644 --- a/tests/Api/Admin/ProductVariantsTest.php +++ b/tests/Api/Admin/ProductVariantsTest.php @@ -296,7 +296,7 @@ final class ProductVariantsTest extends JsonApiTestCase } /** @test */ - public function it_updates_channel_pricing_and_translation_of_a_product_variant(): void + public function it_updates_the_existing_product_variant(): void { $fixtures = $this->loadFixturesFromFiles([ 'authentication/api_administrator.yaml', @@ -317,23 +317,36 @@ final class ProductVariantsTest extends JsonApiTestCase uri: sprintf('/api/v2/admin/product-variants/%s', $productVariant->getCode()), server: $header, content: json_encode([ + 'optionValues' => ['/api/v2/admin/product-option-values/COLOR_RED'], 'channelPricings' => ['WEB' => [ '@id' => sprintf('/api/v2/admin/channel-pricings/%s', $productVariant->getChannelPricingForChannel($channel)->getId()), 'price' => 3000, 'originalPrice' => 4000, - 'minimumPrice' => 210, + 'minimumPrice' => 500, ]], 'translations' => [ 'pl_PL' => [ '@id' => sprintf('/api/v2/admin/product-variant-translations/%s', $productVariant->getTranslation('pl_PL')->getId()), 'locale' => 'pl_PL', - 'name' => 'Pomarańczowy kubek', + 'name' => 'Czerwony kubek', ], 'de_DE' => [ 'locale' => 'de_DE', - 'name' => 'Orange Tasse', + 'name' => 'Rote Tasse', ], - ] + ], + 'enabled' => false, + 'position' => 2, + 'tracked' => false, + 'onHold' => 0, + 'onHand' => 0, + 'weight' => 50.5, + 'width' => 50.5, + 'height' => 50.5, + 'depth' => 50.5, + 'taxCategory' => '/api/v2/admin/tax-categories/special', + 'shippingCategory' => '/api/v2/admin/shipping-categories/special', + 'shippingRequired' => false, ], JSON_THROW_ON_ERROR), ); diff --git a/tests/Api/Responses/Expected/admin/product_variant/put_product_variant_response.json b/tests/Api/Responses/Expected/admin/product_variant/put_product_variant_response.json index debd39034a..d2b67321fe 100644 --- a/tests/Api/Responses/Expected/admin/product_variant/put_product_variant_response.json +++ b/tests/Api/Responses/Expected/admin/product_variant/put_product_variant_response.json @@ -5,7 +5,7 @@ "code": "MUG", "product": "\/api\/v2\/admin\/products\/MUG_SW", "optionValues":[ - "\/api\/v2\/admin\/product-option-values\/COLOR_BLUE" + "\/api\/v2\/admin\/product-option-values\/COLOR_RED" ], "channelPricings": { "WEB": { @@ -15,7 +15,7 @@ "price": 3000, "originalPrice": 4000, "lowestPriceBeforeDiscount": 2000, - "minimumPrice": 210 + "minimumPrice": 500 } }, "translations": { @@ -23,29 +23,29 @@ "@id": "\/api\/v2\/admin\/product-variant-translations\/@integer@", "@type": "ProductVariantTranslation", "id": @integer@, - "name": "Orange Tasse", + "name": "Rote Tasse", "locale": "de_DE" }, "pl_PL": { "@id": "\/api\/v2\/admin\/product-variant-translations\/@integer@", "@type": "ProductVariantTranslation", "id": @integer@, - "name": "Pomarańczowy kubek", + "name": "Czerwony kubek", "locale": "pl_PL" } }, - "enabled": true, - "position": 1, - "tracked": true, + "enabled": false, + "position": 2, + "tracked": false, "onHold": 0, - "onHand": 10, - "weight": 100.5, - "width": 100.5, - "height": 100.5, - "depth": 100.5, - "taxCategory": "\/api\/v2\/admin\/tax-categories\/default", - "shippingCategory": "\/api\/v2\/admin\/shipping-categories\/default", - "shippingRequired": true, + "onHand": 0, + "weight": 50.5, + "width": 50.5, + "height": 50.5, + "depth": 50.5, + "taxCategory": "\/api\/v2\/admin\/tax-categories\/special", + "shippingCategory": "\/api\/v2\/admin\/shipping-categories\/special", + "shippingRequired": false, "createdAt": @date@, "updatedAt": @date@ } From 98a7e3cab2dfdf7ecc1e38dcdee6d36b44dd69fc Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Mon, 16 Oct 2023 11:19:17 +0200 Subject: [PATCH 04/14] [API][Admin] Add DELETE endpoint for product variant resource --- .../ProductVariantDataPersister.php | 49 ++++++++++++++ .../ProductVariantCannotBeRemoved.php | 26 ++++++++ .../config/api_resources/ProductVariant.xml | 5 ++ .../Resources/config/app/config.yaml | 1 + .../config/services/data_persisters.xml | 5 ++ .../ProductVariantDataPersisterSpec.php | 66 +++++++++++++++++++ tests/Api/Admin/ProductVariantsTest.php | 52 +++++++++++++++ .../ORM/product/product_variant.yaml | 21 ++++++ ...elete_product_variant_in_use_response.json | 6 ++ 9 files changed, 231 insertions(+) create mode 100644 src/Sylius/Bundle/ApiBundle/DataPersister/ProductVariantDataPersister.php create mode 100644 src/Sylius/Bundle/ApiBundle/Exception/ProductVariantCannotBeRemoved.php create mode 100644 src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductVariantDataPersisterSpec.php create mode 100644 tests/Api/Responses/Expected/admin/product_variant/delete_product_variant_in_use_response.json diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/ProductVariantDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/ProductVariantDataPersister.php new file mode 100644 index 0000000000..27ef76ad38 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/ProductVariantDataPersister.php @@ -0,0 +1,49 @@ + $context */ + public function supports($data, array $context = []): bool + { + return $data instanceof ProductVariantInterface; + } + + /** @param array $context */ + public function persist($data, array $context = []) + { + return $this->decoratedDataPersister->persist($data, $context); + } + + /** @param array $context */ + public function remove($data, array $context = []) + { + try { + return $this->decoratedDataPersister->remove($data, $context); + } catch (ForeignKeyConstraintViolationException) { + throw new ProductVariantCannotBeRemoved(); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Exception/ProductVariantCannotBeRemoved.php b/src/Sylius/Bundle/ApiBundle/Exception/ProductVariantCannotBeRemoved.php new file mode 100644 index 0000000000..caa581e798 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Exception/ProductVariantCannotBeRemoved.php @@ -0,0 +1,26 @@ +shop:product_variant:read + + + DELETE + /admin/product-variants/{code} + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml b/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml index abe54dca4c..e253b2bf94 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml @@ -54,6 +54,7 @@ api_platform: Sylius\Bundle\ApiBundle\Exception\OrderNoLongerEligibleForPromotion: 422 Sylius\Bundle\ApiBundle\Exception\ProductAttributeCannotBeRemoved: 422 Sylius\Bundle\ApiBundle\Exception\ProductCannotBeRemoved: 422 + Sylius\Bundle\ApiBundle\Exception\ProductVariantCannotBeRemoved: 422 Sylius\Bundle\ApiBundle\Exception\ProvinceCannotBeRemoved: 422 Sylius\Bundle\ApiBundle\Exception\ShippingMethodCannotBeRemoved: 422 Sylius\Bundle\ApiBundle\Exception\TaxonCannotBeRemoved: 422 diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml index 00ae7067dd..16c3805133 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml @@ -64,6 +64,11 @@ + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductVariantDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductVariantDataPersisterSpec.php new file mode 100644 index 0000000000..577df42f4f --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductVariantDataPersisterSpec.php @@ -0,0 +1,66 @@ +beConstructedWith($dataPersister); + } + + function it_is_a_context_aware_persister(): void + { + $this->shouldImplement(ContextAwareDataPersisterInterface::class); + } + + function it_supports_only_product_variant(ProductVariantInterface $productVariant): void + { + $this->supports(new \stdClass())->shouldReturn(false); + $this->supports($productVariant)->shouldReturn(true); + } + + function it_uses_inner_persister_to_persist_product_variant( + ContextAwareDataPersisterInterface $dataPersister, + ProductVariantInterface $productVariant, + ): void { + $dataPersister->persist($productVariant, [])->shouldBeCalled(); + + $this->persist($productVariant); + } + + function it_throws_cannot_be_removed_exception_if_constraint_fails_on_removal( + ContextAwareDataPersisterInterface $dataPersister, + ProductVariantInterface $productVariant, + ): void { + $dataPersister->remove($productVariant, [])->willThrow(ForeignKeyConstraintViolationException::class); + + $this->shouldThrow(ProductVariantCannotBeRemoved::class)->during('remove', [$productVariant]); + } + + function it_uses_inner_persister_to_remove_product_variant( + ContextAwareDataPersisterInterface $dataPersister, + ProductVariantInterface $productVariant, + ): void { + $dataPersister->remove($productVariant, [])->shouldBeCalled(); + + $this->remove($productVariant); + } +} diff --git a/tests/Api/Admin/ProductVariantsTest.php b/tests/Api/Admin/ProductVariantsTest.php index 8c7b1ea3e2..beacad759d 100644 --- a/tests/Api/Admin/ProductVariantsTest.php +++ b/tests/Api/Admin/ProductVariantsTest.php @@ -395,4 +395,56 @@ final class ProductVariantsTest extends JsonApiTestCase $this->assertResponseCode($this->client->getResponse(), Response::HTTP_UNPROCESSABLE_ENTITY); } + + /** @test */ + public function it_deletes_the_product_variant(): void + { + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var ProductVariantInterface $productVariant */ + $productVariant = $fixtures['product_variant_2']; + + $this->client->request( + method: 'DELETE', + uri: sprintf('/api/v2/admin/product-variants/%s', $productVariant->getCode()), + server: $header, + ); + + $this->assertResponseCode($this->client->getResponse(), Response::HTTP_NO_CONTENT); + } + + /** @test */ + public function it_does_not_delete_the_product_variant_in_use(): void + { + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var ProductVariantInterface $productVariant */ + $productVariant = $fixtures['product_variant']; + + $this->client->request( + method: 'DELETE', + uri: sprintf('/api/v2/admin/product-variants/%s', $productVariant->getCode()), + server: $header, + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/product_variant/delete_product_variant_in_use_response', + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } } diff --git a/tests/Api/DataFixtures/ORM/product/product_variant.yaml b/tests/Api/DataFixtures/ORM/product/product_variant.yaml index 08fc01d327..b74d854419 100644 --- a/tests/Api/DataFixtures/ORM/product/product_variant.yaml +++ b/tests/Api/DataFixtures/ORM/product/product_variant.yaml @@ -95,3 +95,24 @@ Sylius\Component\Product\Model\ProductOptionValueTranslation: locale: 'en_US' value: 'Red' translatable: '@product_option_value_color_red' + +Sylius\Component\Core\Model\Customer: + customer: + firstName: 'John' + lastName: 'Doe' + email: 'john.doe@example.com' + emailCanonical: 'john.doe@example.com' + +Sylius\Component\Core\Model\Order: + cart: + channel: '@channel_web' + items: ['@order_item'] + currencyCode: 'USD' + localeCode: 'en_US' + customer: '@customer' + state: 'cart' + +Sylius\Component\Core\Model\OrderItem: + order_item: + variant: '@product_variant' + order: '@cart' diff --git a/tests/Api/Responses/Expected/admin/product_variant/delete_product_variant_in_use_response.json b/tests/Api/Responses/Expected/admin/product_variant/delete_product_variant_in_use_response.json new file mode 100644 index 0000000000..19a85dcb27 --- /dev/null +++ b/tests/Api/Responses/Expected/admin/product_variant/delete_product_variant_in_use_response.json @@ -0,0 +1,6 @@ +{ + "@context": "\/api\/v2\/contexts\/Error", + "@type": "hydra:Error", + "hydra:title": "An error occurred", + "hydra:description": "Cannot delete, the product variant is in use." +} From 7403d1c08e5c7cace0b3af07294e593f0b105d4f Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Tue, 17 Oct 2023 09:59:15 +0200 Subject: [PATCH 05/14] [Behat][API] Cover scenarios for adding a product variant --- .../adding_product_variant.feature | 22 +++---- ...t_variant_with_only_original_price.feature | 4 +- .../Admin/ManagingProductVariantsContext.php | 57 ++++++++++++++++++- .../Transform/ProductOptionValueContext.php | 1 + .../Admin/ManagingProductVariantsContext.php | 6 +- .../api/product/managing_product_variants.yml | 2 + 6 files changed, 74 insertions(+), 18 deletions(-) diff --git a/features/product/managing_product_variants/adding_product_variant.feature b/features/product/managing_product_variants/adding_product_variant.feature index 4362459e45..e76b1b89ae 100644 --- a/features/product/managing_product_variants/adding_product_variant.feature +++ b/features/product/managing_product_variants/adding_product_variant.feature @@ -36,7 +36,7 @@ Feature: Adding a new product variant And the variant with code "VODKA_WYBOROWA_PREMIUM" should be named "Vodka Wyborowa Premium" in "English (United States)" locale And the variant with code "VODKA_WYBOROWA_PREMIUM" should be named "Wódka Wyborowa Premium" in "Polish (Poland)" locale - @ui + @api @ui Scenario: Adding a new product variant with specific option's value When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_MELON" @@ -46,7 +46,7 @@ Feature: Adding a new product variant Then I should be notified that it has been successfully created And the "VODKA_WYBOROWA_MELON" variant of the "Wyborowa Vodka" product should appear in the store - @ui + @api @ui Scenario: Adding a new product variant with specific shipping category When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" @@ -56,7 +56,7 @@ Feature: Adding a new product variant Then I should be notified that it has been successfully created And the "VODKA_WYBOROWA_PREMIUM" variant of the "Wyborowa Vodka" product should appear in the store - @ui + @api @ui Scenario: Adding a new product variant with discounted price When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_EXQUISITE" @@ -65,22 +65,22 @@ Feature: Adding a new product variant And I add it Then I should be notified that it has been successfully created And the "VODKA_WYBOROWA_EXQUISITE" variant of the "Wyborowa Vodka" product should appear in the store - And the variant with code "VODKA_WYBOROWA_EXQUISITE" should be priced at $100.00 for channel "United States" - And the variant with code "VODKA_WYBOROWA_EXQUISITE" should have an original price of $120.00 for channel "United States" + And the variant with code "VODKA_WYBOROWA_EXQUISITE" should be priced at "$100.00" for channel "United States" + And the variant with code "VODKA_WYBOROWA_EXQUISITE" should have an original price of "$120.00" for channel "United States" - @ui + @api @ui Scenario: Adding a new product variant without shipping required When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" And I set its price to "$100.00" for "United States" channel - And I do not want to have shipping required for this product + And I do not want to have shipping required for this product variant And I add it Then I should be notified that it has been successfully created And the variant with code "VODKA_WYBOROWA_PREMIUM" should not have shipping required And the "VODKA_WYBOROWA_PREMIUM" variant of the "Wyborowa Vodka" product should appear in the store - And the variant with code "VODKA_WYBOROWA_PREMIUM" should be priced at $100.00 for channel "United States" + And the variant with code "VODKA_WYBOROWA_PREMIUM" should be priced at "$100.00" for channel "United States" - @ui + @api @ui Scenario: Adding a new free product variant When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" @@ -88,9 +88,9 @@ Feature: Adding a new product variant And I add it Then I should be notified that it has been successfully created And the "VODKA_WYBOROWA_PREMIUM" variant of the "Wyborowa Vodka" product should appear in the store - And the variant with code "VODKA_WYBOROWA_PREMIUM" should be priced at $0.00 for channel "United States" + And the variant with code "VODKA_WYBOROWA_PREMIUM" should be priced at "$0.00" for channel "United States" - @ui @api + @api @ui Scenario: Adding a new product variant with minimum price When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA" diff --git a/features/product/managing_product_variants/adding_product_variant_with_only_original_price.feature b/features/product/managing_product_variants/adding_product_variant_with_only_original_price.feature index 7021a2b985..051607d794 100644 --- a/features/product/managing_product_variants/adding_product_variant_with_only_original_price.feature +++ b/features/product/managing_product_variants/adding_product_variant_with_only_original_price.feature @@ -10,7 +10,7 @@ Feature: Adding a product variant with only original price And this product is disabled in "United States" channel And I am logged in as an administrator - @ui + @api @ui Scenario: Adding a new product variant without price When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_DELUX" @@ -18,4 +18,4 @@ Feature: Adding a product variant with only original price And I add it Then I should be notified that it has been successfully created And the "VODKA_WYBOROWA_DELUX" variant of the "Wyborowa Vodka" product should appear in the store - And the variant with code "VODKA_WYBOROWA_DELUX" should be originally priced at $100.00 for channel "United States" + And the variant with code "VODKA_WYBOROWA_DELUX" should be originally priced at "$100.00" for channel "United States" diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php index 29ad1b1b6a..34a04debbb 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php @@ -22,6 +22,8 @@ use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\ProductVariantInterface; +use Sylius\Component\Product\Model\ProductOptionValueInterface; +use Sylius\Component\Shipping\Model\ShippingCategoryInterface; use Webmozart\Assert\Assert; final class ManagingProductVariantsContext implements Context @@ -175,6 +177,30 @@ final class ManagingProductVariantsContext implements Context ]); } + /** + * @When I set its :optionName option to :optionValue + */ + public function iSetItsOptionAs(string $optionName, ProductOptionValueInterface $optionValue): void + { + $this->client->addRequestData('optionValues', [$this->iriConverter->getIriFromResource($optionValue)]); + } + + /** + * @When I set its shipping category as :shippingCategory + */ + public function iSetItsShippingCategoryAs(ShippingCategoryInterface $shippingCategory): void + { + $this->client->addRequestData('shippingCategory', $this->iriConverter->getIriFromResource($shippingCategory)); + } + + /** + * @When I do not want to have shipping required for this product variant + */ + public function iDoNotWantToHaveShippingRequiredForThisProductVariant(): void + { + $this->client->addRequestData('shippingRequired', false); + } + /** * @Then I should be notified that it has been successfully created */ @@ -221,15 +247,32 @@ final class ManagingProductVariantsContext implements Context } /** - * @Then /^the (variant with code "[^"]+") should be priced at ("[^"]+") for (channel "([^"]+)")$/ + * @Then /^the variant with code "([^"]+)" should be priced at ("[^"]+") for (channel "[^"]+")$/ */ - public function theVariantWithCodeShouldBePricedAtForChannel(ProductVariantInterface $productVariant, int $price, ChannelInterface $channel): void - { + public function theVariantWithCodeShouldBePricedAtForChannel( + string $variantCode, + int $price, + ChannelInterface $channel, + ): void { $response = $this->responseChecker->getCollection($this->client->index(Resources::PRODUCT_VARIANTS)); Assert::same($response[self::FIRST_COLLECTION_ITEM]['channelPricings'][$channel->getCode()]['price'], $price); } + /** + * @Then /^the variant with code "([^"]+)" should have an original price of ("[^"]+") for (channel "[^"]+")$/ + * @Then /^the variant with code "([^"]+)" should be originally priced at ("[^"]+") for (channel "[^"]+")$/ + */ + public function theVariantWithCodeShouldHaveAnOriginalPriceOfForChannel( + string $variantCode, + int $originalPrice, + ChannelInterface $channel, + ): void { + $response = $this->responseChecker->getCollection($this->client->index(Resources::PRODUCT_VARIANTS)); + + Assert::same($response[self::FIRST_COLLECTION_ITEM]['channelPricings'][$channel->getCode()]['originalPrice'], $originalPrice); + } + /** * @Then /^the (variant with code "[^"]+") should have minimum price ("[^"]+") for (channel "([^"]+)")$/ */ @@ -240,6 +283,14 @@ final class ManagingProductVariantsContext implements Context Assert::same($response[self::FIRST_COLLECTION_ITEM]['channelPricings'][$channel->getCode()]['minimumPrice'], $minimumPrice); } + /** + * @Then /^the (variant with code "[^"]+") should not have shipping required$/ + */ + public function theVariantWithCodeShouldNotHaveShippingRequired(ProductVariantInterface $productVariant): void + { + Assert::false($this->responseChecker->getValue($this->client->getLastResponse(), 'shippingRequired')); + } + private function updateChannelPricingField( ProductVariantInterface $variant, ChannelInterface $channel, diff --git a/src/Sylius/Behat/Context/Transform/ProductOptionValueContext.php b/src/Sylius/Behat/Context/Transform/ProductOptionValueContext.php index f95a4268a0..6711f2aa47 100644 --- a/src/Sylius/Behat/Context/Transform/ProductOptionValueContext.php +++ b/src/Sylius/Behat/Context/Transform/ProductOptionValueContext.php @@ -26,6 +26,7 @@ final class ProductOptionValueContext implements Context /** * @Transform /^"([^"]+)" option value$/ + * @Transform :optionValue */ public function getProductOptionValueByCode(string $code): ProductOptionValueInterface { diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php index 68f427c8bc..de8814cf25 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php @@ -180,9 +180,9 @@ final class ManagingProductVariantsContext implements Context } /** - * @When I do not want to have shipping required for this product + * @When I do not want to have shipping required for this product( variant) */ - public function iDoNotWantToHaveShippingRequiredForThisProduct() + public function iDoNotWantToHaveShippingRequiredForThisProduct(): void { $this->createPage->setShippingRequired(false); } @@ -374,6 +374,7 @@ final class ManagingProductVariantsContext implements Context /** * @Then /^the (variant with code "[^"]+") should be originally priced at (?:€|£|\$)([^"]+) for (channel "[^"]+")$/ + * @Then /^the (variant with code "[^"]+") should be originally priced at "(?:€|£|\$)([^"]+)" for (channel "[^"]+")$/ */ public function theVariantWithCodeShouldBeOriginalPricedAtForChannel( ProductVariantInterface $productVariant, @@ -397,6 +398,7 @@ final class ManagingProductVariantsContext implements Context /** * @Then /^the (variant with code "[^"]+") should have an original price of (?:€|£|\$)([^"]+) for (channel "([^"]+)")$/ + * @Then /^the (variant with code "[^"]+") should have an original price of "(?:€|£|\$)([^"]+)" for (channel "([^"]+)")$/ */ public function theVariantWithCodeShouldHaveAnOriginalPriceOfForChannel(ProductVariantInterface $productVariant, $originalPrice, ChannelInterface $channel) { diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_variants.yml b/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_variants.yml index fab1a64856..7b72ffa96b 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_variants.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_variants.yml @@ -12,8 +12,10 @@ default: - sylius.behat.context.transform.locale - sylius.behat.context.transform.lexical - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_option_value - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shared_storage + - sylius.behat.context.transform.shipping_category - sylius.behat.context.transform.channel - sylius.behat.context.transform.customer - sylius.behat.context.transform.payment From 76c39a669152ba7b05b2c056467afe291df2474f Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Tue, 17 Oct 2023 12:11:37 +0200 Subject: [PATCH 06/14] [Behat][API] Cover scenarios for browsing product variants --- .../browsing_product_variants.feature | 8 +-- .../Admin/ManagingProductVariantsContext.php | 51 +++++++++++++++++++ .../Transform/ProductVariantContext.php | 1 + .../config/api_resources/ProductVariant.xml | 1 + 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/features/product/managing_product_variants/browsing_product_variants.feature b/features/product/managing_product_variants/browsing_product_variants.feature index 77b9be9430..506a6f1f7a 100644 --- a/features/product/managing_product_variants/browsing_product_variants.feature +++ b/features/product/managing_product_variants/browsing_product_variants.feature @@ -10,23 +10,23 @@ Feature: Browsing product variants And the product "Wyborowa Vodka" has "Wyborowa Vodka Exquisite" variant priced at "$40.00" And I am logged in as an administrator - @ui + @api @ui Scenario: Browsing product variants in store When I want to view all variants of this product Then I should see 1 variant in the list - @ui + @api @ui Scenario: Being informed that product variant is not tracked When I want to view all variants of this product Then I should see that the "Wyborowa Vodka Exquisite" variant is not tracked - @ui + @api @ui Scenario: Being informed about on hand quantity of a product variant Given the "Wyborowa Vodka Exquisite" product variant is tracked by the inventory When I want to view all variants of this product Then I should see that the "Wyborowa Vodka Exquisite" variant has zero on hand quantity - @ui + @api @ui Scenario: Being informed that product variant is enabled When I want to view all variants of this product Then I should see that the "Wyborowa Vodka Exquisite" variant is enabled diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php index 34a04debbb..0ebafe0145 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php @@ -201,6 +201,16 @@ final class ManagingProductVariantsContext implements Context $this->client->addRequestData('shippingRequired', false); } + /** + * @When /^I want to view all variants of (this product)$/ + */ + public function iWantToViewAllVariantsOfThisProduct(ProductInterface $product): void + { + $this->client->index(Resources::PRODUCT_VARIANTS); + $this->client->addFilter('product', $this->iriConverter->getIriFromResource($product)); + $this->client->filter(); + } + /** * @Then I should be notified that it has been successfully created */ @@ -291,6 +301,47 @@ final class ManagingProductVariantsContext implements Context Assert::false($this->responseChecker->getValue($this->client->getLastResponse(), 'shippingRequired')); } + /** + * @Then I should see :amount variant(s) in the list + */ + public function iShouldSeeNumberOfProductVariantsInTheList(int $amount): void + { + Assert::count($this->responseChecker->getCollection($this->client->getLastResponse()), $amount); + } + + /** + * @Then I should see that the :productVariant variant is not tracked + */ + public function iShouldSeeThatVariantIsNotTracked(ProductVariantInterface $productVariant): void + { + Assert::true($this->responseChecker->hasItemWithValues( + $this->client->getLastResponse(), + ['code' => $productVariant->getCode(), 'tracked' => false], + )); + } + + /** + * @Then I should see that the :productVariant variant has zero on hand quantity + */ + public function iShouldSeeThatTheVariantHasZeroOnHandQuantity(ProductVariantInterface $productVariant): void + { + Assert::true($this->responseChecker->hasItemWithValues( + $this->client->getLastResponse(), + ['code' => $productVariant->getCode(), 'onHand' => 0], + )); + } + + /** + * @Then I should see that the :productVariant variant is enabled + */ + public function iShouldSeeThatTheVariantIsEnabled(ProductVariantInterface $productVariant): void + { + Assert::true($this->responseChecker->hasItemWithValues( + $this->client->getLastResponse(), + ['code' => $productVariant->getCode(), 'enabled' => true], + )); + } + private function updateChannelPricingField( ProductVariantInterface $variant, ChannelInterface $channel, diff --git a/src/Sylius/Behat/Context/Transform/ProductVariantContext.php b/src/Sylius/Behat/Context/Transform/ProductVariantContext.php index 1a762683e6..c2998855c9 100644 --- a/src/Sylius/Behat/Context/Transform/ProductVariantContext.php +++ b/src/Sylius/Behat/Context/Transform/ProductVariantContext.php @@ -71,6 +71,7 @@ final class ProductVariantContext implements Context /** * @Transform /^"([^"]+)" product variant$/ * @Transform /^"([^"]+)" variant$/ + * @Transform :productVariant * @Transform :variant */ public function getProductVariantByName($name) diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductVariant.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductVariant.xml index c8c999e64f..43e9ebc768 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductVariant.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductVariant.xml @@ -57,6 +57,7 @@ GET /admin/product-variants + sylius.api.product_variant_product_filter Sylius\Bundle\ApiBundle\Filter\Doctrine\ProductVariantCatalogPromotionFilter From d8adf86ee6c299bd3d2b91f4198576b7891c5a24 Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Tue, 17 Oct 2023 13:03:28 +0200 Subject: [PATCH 07/14] [Behat][API] Cover scenarios for deleting a product variant --- .../deleting_product_variant.feature | 4 +- ...tion_of_purchased_product_variant.feature} | 4 +- .../Admin/ManagingProductVariantsContext.php | 57 +++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) rename features/product/managing_product_variants/{prevent_deletion_of_purchased_product_variant.feature => preventing_deletion_of_purchased_product_variant.feature} (92%) diff --git a/features/product/managing_product_variants/deleting_product_variant.feature b/features/product/managing_product_variants/deleting_product_variant.feature index 5f3dfde191..9137386760 100644 --- a/features/product/managing_product_variants/deleting_product_variant.feature +++ b/features/product/managing_product_variants/deleting_product_variant.feature @@ -10,8 +10,8 @@ Feature: Deleting a product variant And the product "PHP Mug" has "Medium PHP Mug" variant priced at "$40.00" And I am logged in as an administrator - @domain @ui - Scenario: Deleted variant disappears from the product catalog + @api @domain @ui + Scenario: Deleting a product variant from the product catalog When I delete the "Medium PHP Mug" variant of product "PHP Mug" Then I should be notified that it has been successfully deleted And this variant should not exist in the product catalog diff --git a/features/product/managing_product_variants/prevent_deletion_of_purchased_product_variant.feature b/features/product/managing_product_variants/preventing_deletion_of_purchased_product_variant.feature similarity index 92% rename from features/product/managing_product_variants/prevent_deletion_of_purchased_product_variant.feature rename to features/product/managing_product_variants/preventing_deletion_of_purchased_product_variant.feature index 83f7d4a007..c14a2378c5 100644 --- a/features/product/managing_product_variants/prevent_deletion_of_purchased_product_variant.feature +++ b/features/product/managing_product_variants/preventing_deletion_of_purchased_product_variant.feature @@ -15,8 +15,8 @@ Feature: Prevent deletion of purchased product variant And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment And I am logged in as an administrator - @domain @ui - Scenario: Purchased product variant cannot be deleted + @api @domain @ui + Scenario: Being unable to delete a purchased product variant When I try to delete the "Medium PHP Mug" variant of product "PHP Mug" Then I should be notified that this variant is in use and cannot be deleted And this variant should still exist in the product catalog diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php index 0ebafe0145..80d196c840 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php @@ -211,6 +211,15 @@ final class ManagingProductVariantsContext implements Context $this->client->filter(); } + /** + * @When /^I delete the ("[^"]+" variant of product "[^"]+")$/ + * @When /^I try to delete the ("[^"]+" variant of product "[^"]+")$/ + */ + public function iDeleteTheVariantOfProduct(ProductVariantInterface $productVariant): void + { + $this->client->delete(Resources::PRODUCT_VARIANTS, $productVariant->getCode()); + } + /** * @Then I should be notified that it has been successfully created */ @@ -342,6 +351,54 @@ final class ManagingProductVariantsContext implements Context )); } + /** + * @Then I should be notified that it has been successfully deleted + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyDeleted(): void + { + Assert::true( + $this->responseChecker->isDeletionSuccessful($this->client->getLastResponse()), + 'Product variant could not be deleted', + ); + } + + /** + * @Then /^(this variant) should not exist in the product catalog$/ + */ + public function thisProductVariantShouldNotExistInTheProductCatalog(ProductVariantInterface $productVariant): void + { + Assert::false( + $this->responseChecker->hasItemWithValue( + $this->client->index(Resources::PRODUCT_VARIANTS), 'code', $productVariant->getCode(), + ), + 'The product variant still exists, but it should not', + ); + } + + /** + * @Then /^(this variant) should still exist in the product catalog$/ + */ + public function thisProductVariantShouldStillExistInTheProductCatalog(ProductVariantInterface $productVariant): void + { + Assert::true( + $this->responseChecker->hasItemWithValue( + $this->client->index(Resources::PRODUCT_VARIANTS), 'code', $productVariant->getCode(), + ), + 'The product variant does not exist, but it should', + ); + } + + /** + * @Then I should be notified that this variant is in use and cannot be deleted + */ + public function iShouldBeNotifiedThatThisVariantIsInUseAndCannotBeDeleted(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Cannot delete, the product variant is in use.', + ); + } + private function updateChannelPricingField( ProductVariantInterface $variant, ChannelInterface $channel, From 42a0c129ad8236a751a4aed3a55dcb7d0574044e Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Tue, 17 Oct 2023 13:40:50 +0200 Subject: [PATCH 08/14] [API][Behat] Mark scenarios for generating product variants as no-api --- .../generating_product_variant.feature | 8 ++++---- .../generating_product_variant_validation.feature | 10 +++++----- ...ts_variants_from_options_without_any_values.feature | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/features/product/managing_product_variants/generating_product_variant.feature b/features/product/managing_product_variants/generating_product_variant.feature index deafaabfd1..1af98cc99a 100644 --- a/features/product/managing_product_variants/generating_product_variant.feature +++ b/features/product/managing_product_variants/generating_product_variant.feature @@ -10,7 +10,7 @@ Feature: Generating product variants And this product has option "Taste" with values "Orange" and "Melon" And I am logged in as an administrator - @ui + @ui @no-api Scenario: Generating a product variant for product without variants When I want to generate new variants for this product And I specify that the 1st variant is identified by "WYBOROWA_ORANGE" code and costs "$90" in "United States" channel @@ -19,7 +19,7 @@ Feature: Generating product variants Then I should be notified that it has been successfully generated And I should see 2 variants in the list - @ui + @ui @no-api Scenario: Generating the rest of product variants for product with at least one Given this product is available in "Melon" taste priced at "$95.00" When I want to generate new variants for this product @@ -28,7 +28,7 @@ Feature: Generating product variants Then I should be notified that it has been successfully generated And I should see 2 variants in the list - @ui + @ui @no-api Scenario: Generating the rest of product variants for product with at least one Given this product is available in "Orange" taste priced at "$90.00" When I want to generate new variants for this product @@ -37,7 +37,7 @@ Feature: Generating product variants Then I should be notified that it has been successfully generated And I should see 2 variants in the list - @ui @javascript + @ui @javascript @no-api Scenario: Generating only a part of product variants When I want to generate new variants for this product And I specify that the 1st variant is identified by "WYBOROWA_ORANGE" code and costs "$90" in "United States" channel diff --git a/features/product/managing_product_variants/generating_product_variant_validation.feature b/features/product/managing_product_variants/generating_product_variant_validation.feature index 68e3e2867b..bd4049aa96 100644 --- a/features/product/managing_product_variants/generating_product_variant_validation.feature +++ b/features/product/managing_product_variants/generating_product_variant_validation.feature @@ -10,7 +10,7 @@ Feature: Generating product variant generation And this product has option "Taste" with values "Orange" and "Melon" And I am logged in as an administrator - @ui + @ui @no-api Scenario: Generating a product's variant without price When I want to generate new variants for this product And I specify that the 1st variant is identified by "WYBOROWA_ORANGE" code @@ -18,7 +18,7 @@ Feature: Generating product variant generation Then I should be notified that prices in all channels must be defined for the 1st variant And I should not see any variants in the list - @ui + @ui @no-api Scenario: Generating a product's variant without code When I want to generate new variants for this product And I specify that the 1st variant costs "$90" in "United States" channel @@ -26,7 +26,7 @@ Feature: Generating product variant generation Then I should be notified that code is required for the 1st variant And I should not see any variants in the list - @ui + @ui @no-api Scenario: Generating product's variants without specific required fields for second variant When I want to generate new variants for this product And I specify that the 1st variant is identified by "WYBOROWA_ORANGE" code @@ -36,7 +36,7 @@ Feature: Generating product variant generation Then I should be notified that prices in all channels must be defined for the 2nd variant And I should not see any variants in the list - @ui + @ui @no-api Scenario: Generating product's variants with the same code When I want to generate new variants for this product And I specify that the 1st variant is identified by "WYBOROWA_TASTE" code @@ -48,7 +48,7 @@ Feature: Generating product variant generation And I should be notified that variant code must be unique within this product for the 2nd variant And I should not see any variants in the list - @ui + @ui @no-api Scenario: Generating product's variants without specific required fields for second variant When I want to generate new variants for this product And I do not specify any information about variants diff --git a/features/product/managing_product_variants/preventing_the_generation_of_products_variants_from_options_without_any_values.feature b/features/product/managing_product_variants/preventing_the_generation_of_products_variants_from_options_without_any_values.feature index 2e7a71af67..677bb405d3 100644 --- a/features/product/managing_product_variants/preventing_the_generation_of_products_variants_from_options_without_any_values.feature +++ b/features/product/managing_product_variants/preventing_the_generation_of_products_variants_from_options_without_any_values.feature @@ -10,7 +10,7 @@ Feature: Preventing the generation of product variants from options without any And this product has an option "Taste" without any values And I am logged in as an administrator - @ui + @ui @no-api Scenario: Trying to generate a product variant for a product without options values When I try to generate new variants for this product Then I should be notified that variants cannot be generated from options without any values From e5d9a161dc710a749b170557a40759bd44389331 Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Tue, 17 Oct 2023 15:40:43 +0200 Subject: [PATCH 09/14] [API][Behat] Cover scenarios for editing and validating product variants --- .../editing_product_variant.feature | 2 +- .../product_variant_validation.feature | 16 +- ...ariant_price_from_obsolete_channel.feature | 10 +- ...ntory_tracking_on_product_variant.feature} | 10 +- ...ature => toggling_product_variant.feature} | 10 +- ...roduct_price_from_obsolete_channel.feature | 4 +- .../Admin/ManagingProductVariantsContext.php | 237 +++++++++++++++++- .../Admin/ManagingProductVariantsContext.php | 10 +- .../Ui/Admin/ManagingProductsContext.php | 2 +- 9 files changed, 266 insertions(+), 35 deletions(-) rename features/product/managing_product_variants/{disable_or_enable_inventory_on_product_variant.feature => toggling_inventory_tracking_on_product_variant.feature} (84%) rename features/product/managing_product_variants/{disable_or_enable_product_variant.feature => toggling_product_variant.feature} (87%) diff --git a/features/product/managing_product_variants/editing_product_variant.feature b/features/product/managing_product_variants/editing_product_variant.feature index 37275e438c..81575bc11f 100644 --- a/features/product/managing_product_variants/editing_product_variant.feature +++ b/features/product/managing_product_variants/editing_product_variant.feature @@ -15,7 +15,7 @@ Feature: Editing a product variant @api @ui Scenario: Changing product variant name - Given I want to modify the "Go" product variant + When I want to modify the "Go" product variant And I name it "Java" in "English (United States)" And I name it "Kawa" in "Polish (Poland)" And I save my changes diff --git a/features/product/managing_product_variants/product_variant_validation.feature b/features/product/managing_product_variants/product_variant_validation.feature index 224abad4d7..db1d21147e 100644 --- a/features/product/managing_product_variants/product_variant_validation.feature +++ b/features/product/managing_product_variants/product_variant_validation.feature @@ -9,7 +9,7 @@ Feature: Product variant validation And the store has a "Wyborowa Vodka" configurable product And I am logged in as an administrator - @ui + @api @ui Scenario: Adding a new product variant without specifying its price When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" @@ -18,16 +18,16 @@ Feature: Product variant validation Then I should be notified that prices in all channels must be defined And the "VODKA_WYBOROWA_PREMIUM" variant of the "Wyborowa Vodka" product should not appear in the store - @ui + @api @ui Scenario: Adding a new product variant with price below 0 When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" - And I set its price to "$-60.00" for "United States" channel + And I set its price to "-$60.00" for "United States" channel And I try to add it Then I should be notified that price cannot be lower than 0 And the "VODKA_WYBOROWA_PREMIUM" variant of the "Wyborowa Vodka" product should not appear in the store - @ui + @api @ui Scenario: Adding a new product variant without specifying its code When I want to create a new variant of this product And I set its price to "$80.00" for "United States" channel @@ -36,7 +36,7 @@ Feature: Product variant validation Then I should be notified that code is required And the "Wyborowa Vodka" product should have no variants - @ui + @api @ui Scenario: Adding a new product variant with duplicated code Given this product has "Wyborowa Exquisite" variant priced at "$90" identified by "VODKA_WYBOROWA_PREMIUM" When I want to create a new variant of this product @@ -46,7 +46,7 @@ Feature: Product variant validation Then I should be notified that code has to be unique And the "Wyborowa Vodka" product should have only one variant - @ui + @api @ui Scenario: Adding a new product variant with same set of options Given this product has option "Taste" with values "Orange" and "Melon" And this product is available in "Melon" taste priced at "$95.00" @@ -58,7 +58,7 @@ Feature: Product variant validation Then I should be notified that this variant already exists And the "Wyborowa Vodka" product should have only one variant - @ui + @api @ui Scenario: Adding a new product variant with negative properties When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" @@ -68,7 +68,7 @@ Feature: Product variant validation Then I should be notified that height, width, depth and weight cannot be lower than 0 And the "VODKA_WYBOROWA_PREMIUM" variant of the "Wyborowa Vodka" product should not appear in the store - @ui + @api @ui Scenario: Adding a new product variant without current stock When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" diff --git a/features/product/managing_product_variants/removing_product_variant_price_from_obsolete_channel.feature b/features/product/managing_product_variants/removing_product_variant_price_from_obsolete_channel.feature index 71ce655773..9b61bf5f02 100644 --- a/features/product/managing_product_variants/removing_product_variant_price_from_obsolete_channel.feature +++ b/features/product/managing_product_variants/removing_product_variant_price_from_obsolete_channel.feature @@ -14,19 +14,19 @@ Feature: Removing a product variant's price from obsolete channel And this product is disabled in "Web-GB" channel And I am logged in as an administrator - @ui - Scenario: Removing a product variant's price from disabled channel + @api @ui + Scenario: Removing a product variant's price When I want to modify the "Medium PHP Mug" product variant - And I remove its price for "Web-GB" channel + And I remove its price from "Web-GB" channel And I save my changes Then I should not have configured price for "Web-GB" channel But I should have original price equal to "£50.00" in "Web-GB" channel - @ui + @api @ui Scenario: Removing a product variant's price from disabled channel Given the channel "Web-GB" has been disabled When I want to modify the "Medium PHP Mug" product variant - And I remove its price for "Web-GB" channel + And I remove its price from "Web-GB" channel And I save my changes Then I should not have configured price for "Web-GB" channel But I should have original price equal to "£50.00" in "Web-GB" channel diff --git a/features/product/managing_product_variants/disable_or_enable_inventory_on_product_variant.feature b/features/product/managing_product_variants/toggling_inventory_tracking_on_product_variant.feature similarity index 84% rename from features/product/managing_product_variants/disable_or_enable_inventory_on_product_variant.feature rename to features/product/managing_product_variants/toggling_inventory_tracking_on_product_variant.feature index 343c815323..4f93e74b45 100644 --- a/features/product/managing_product_variants/disable_or_enable_inventory_on_product_variant.feature +++ b/features/product/managing_product_variants/toggling_inventory_tracking_on_product_variant.feature @@ -1,5 +1,5 @@ @managing_product_variants -Feature: Toggle the inventory tracking +Feature: Toggling the inventory tracking In order to have the inventory tracked in my shop As an Administrator I want to toggle the inventory tracking @@ -10,8 +10,8 @@ Feature: Toggle the inventory tracking And the product "Wyborowa Vodka" has a "Wyborowa Vodka Exquisite" variant priced at "$40.00" And I am logged in as an administrator - @ui - Scenario: Disabling inventory for a product variant + @api @ui + Scenario: Disabling inventory tracking for the product variant Given the "Wyborowa Vodka Exquisite" product variant is tracked by the inventory When I want to modify the "Wyborowa Vodka Exquisite" product variant And I disable its inventory tracking @@ -19,8 +19,8 @@ Feature: Toggle the inventory tracking Then I should be notified that it has been successfully edited And inventory of this variant should not be tracked - @ui - Scenario: Enabling inventory for a product variant + @api @ui + Scenario: Enabling inventory tracking for the product variant When I want to modify the "Wyborowa Vodka Exquisite" product variant And I enable its inventory tracking And I save my changes diff --git a/features/product/managing_product_variants/disable_or_enable_product_variant.feature b/features/product/managing_product_variants/toggling_product_variant.feature similarity index 87% rename from features/product/managing_product_variants/disable_or_enable_product_variant.feature rename to features/product/managing_product_variants/toggling_product_variant.feature index a54e2f15a3..8a7f37757e 100644 --- a/features/product/managing_product_variants/disable_or_enable_product_variant.feature +++ b/features/product/managing_product_variants/toggling_product_variant.feature @@ -1,5 +1,5 @@ @managing_product_variants -Feature: Toggle the product variant +Feature: Toggling the product variant In order to stop or resume the sale of some product variants As an Administrator I want to toggle the product variant @@ -10,8 +10,8 @@ Feature: Toggle the product variant And the product "Wyborowa Vodka" has a "Wyborowa Vodka Exquisite" variant priced at "$40.00" And I am logged in as an administrator - @ui - Scenario: Disabling a product variant + @api @ui + Scenario: Disabling the product variant Given the "Wyborowa Vodka Exquisite" product variant is enabled When I want to modify the "Wyborowa Vodka Exquisite" product variant And I disable it @@ -19,8 +19,8 @@ Feature: Toggle the product variant Then I should be notified that it has been successfully edited And this variant should be disabled - @ui - Scenario: Enabling a product variant + @api @ui + Scenario: Enabling the product variant Given the "Wyborowa Vodka Exquisite" product variant is disabled When I want to modify the "Wyborowa Vodka Exquisite" product variant And I enable it diff --git a/features/product/managing_products/removing_product_price_from_obsolete_channel.feature b/features/product/managing_products/removing_product_price_from_obsolete_channel.feature index 34333a83cd..aab424551b 100644 --- a/features/product/managing_products/removing_product_price_from_obsolete_channel.feature +++ b/features/product/managing_products/removing_product_price_from_obsolete_channel.feature @@ -17,7 +17,7 @@ Feature: Removing a product's price from the channel where it is not available i Scenario: Removing a product's price from disabled channel Given the channel "Web-GB" has been disabled When I want to modify the "Dice Brewing" product - And I remove its price for "Web-GB" channel + And I remove its price from "Web-GB" channel And I save my changes Then I should not have configured price for "Web-GB" channel But I should have original price equal to "£70.00" in "Web-GB" channel @@ -26,7 +26,7 @@ Feature: Removing a product's price from the channel where it is not available i Scenario: Removing a product's price from obsolete channel Given this product is disabled in "Web-GB" channel When I want to modify the "Dice Brewing" product - And I remove its price for "Web-GB" channel + And I remove its price from "Web-GB" channel And I save my changes Then I should not have configured price for "Web-GB" channel But I should have original price equal to "£70.00" in "Web-GB" channel diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php index 80d196c840..43505047a3 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php @@ -80,6 +80,34 @@ final class ManagingProductVariantsContext implements Context ]); } + /** + * @When I remove its price from :channel channel + */ + public function iRemoveItsPriceForChannel(ChannelInterface $channel): void + { + $content = $this->client->getContent(); + $content['channelPricings'][$channel->getCode()]['price'] = null; + + $this->client->setRequestData($content); + } + + /** + * @When I do not set its price + * @When I do not specify its code + */ + public function iDoNotSetValue(): void + { + // Intentionally left blank + } + + /** + * @When I do not specify its current stock + */ + public function iDoNotSpecifyItsCurrentStock(): void + { + $this->client->addRequestData('onHand', null); + } + /** * @When /^I set its original price to ("[^"]+") for ("[^"]+" channel)$/ */ @@ -105,7 +133,7 @@ final class ManagingProductVariantsContext implements Context } /** - * @When I add it + * @When I( try to) add it */ public function iAddIt(): void { @@ -220,6 +248,51 @@ final class ManagingProductVariantsContext implements Context $this->client->delete(Resources::PRODUCT_VARIANTS, $productVariant->getCode()); } + /** + * @When I disable it + */ + public function iDisableIt(): void + { + $this->client->updateRequestData(['enabled' => false]); + } + + /** + * @When I enable it + */ + public function iEnableIt(): void + { + $this->client->updateRequestData(['enabled' => true]); + } + + /** + * @When I disable its inventory tracking + */ + public function iDisableItsTracking(): void + { + $this->client->updateRequestData(['tracked' => false]); + } + + /** + * @When I enable its inventory tracking + */ + public function iEnableItsTracking(): void + { + $this->client->updateRequestData(['tracked' => true]); + } + + /** + * @When I set its height, width, depth and weight to :value + */ + public function iSetItsDimensionsTo(float $value): void + { + $this->client->updateRequestData([ + 'height' => $value, + 'width' => $value, + 'depth' => $value, + 'weight' => $value, + ]); + } + /** * @Then I should be notified that it has been successfully created */ @@ -235,15 +308,25 @@ final class ManagingProductVariantsContext implements Context } /** - * @Then the :productVariantCode variant of the :product product should appear in the store + * @Then the :productVariantCode variant of the :productName product should appear in the store */ - public function theProductVariantShouldAppearInTheShop(string $productVariantCode, ProductInterface $product): void + public function theProductVariantShouldAppearInTheShop(string $productVariantCode, string $productName): void { $response = $this->client->index(Resources::PRODUCT_VARIANTS); Assert::true($this->responseChecker->hasItemWithValue($response, 'code', $productVariantCode)); } + /** + * @Then the :productVariantCode variant of the :productName product should not appear in the store + */ + public function theProductVariantShouldNotAppearInTheShop(string $productVariantCode, string $productName): void + { + $response = $this->client->index(Resources::PRODUCT_VARIANTS); + + Assert::false($this->responseChecker->hasItemWithValue($response, 'code', $productVariantCode)); + } + /** * @Then /^the (?:variant with code "[^"]+") should be named "([^"]+)" in ("([^"]+)" locale)$/ */ @@ -267,11 +350,12 @@ final class ManagingProductVariantsContext implements Context /** * @Then /^the variant with code "([^"]+)" should be priced at ("[^"]+") for (channel "[^"]+")$/ + * @Then I should not have configured price for :channel channel */ public function theVariantWithCodeShouldBePricedAtForChannel( - string $variantCode, - int $price, - ChannelInterface $channel, + ?string $variantCode = null, + ?int $price = null, + ?ChannelInterface $channel = null, ): void { $response = $this->responseChecker->getCollection($this->client->index(Resources::PRODUCT_VARIANTS)); @@ -283,7 +367,7 @@ final class ManagingProductVariantsContext implements Context * @Then /^the variant with code "([^"]+)" should be originally priced at ("[^"]+") for (channel "[^"]+")$/ */ public function theVariantWithCodeShouldHaveAnOriginalPriceOfForChannel( - string $variantCode, + ?string $variantCode, int $originalPrice, ChannelInterface $channel, ): void { @@ -292,6 +376,16 @@ final class ManagingProductVariantsContext implements Context Assert::same($response[self::FIRST_COLLECTION_ITEM]['channelPricings'][$channel->getCode()]['originalPrice'], $originalPrice); } + /** + * @Then /^I should have original price equal to ("[^"]+") in ("[^"]+" channel)$/ + */ + public function iShouldHaveOriginalPriceEqualToInChannel( + int $originalPrice, + ChannelInterface $channel, + ): void { + $this->theVariantWithCodeShouldHaveAnOriginalPriceOfForChannel(null, $originalPrice, $channel); + } + /** * @Then /^the (variant with code "[^"]+") should have minimum price ("[^"]+") for (channel "([^"]+)")$/ */ @@ -351,6 +445,22 @@ final class ManagingProductVariantsContext implements Context )); } + /** + * @Then this variant should be disabled + */ + public function thisVariantShouldBeDisabled(): void + { + Assert::true($this->responseChecker->hasValue($this->client->getLastResponse(), 'enabled', false)); + } + + /** + * @Then this variant should be enabled + */ + public function thisVariantShouldBeEnabled(): void + { + Assert::true($this->responseChecker->hasValue($this->client->getLastResponse(), 'enabled', true)); + } + /** * @Then I should be notified that it has been successfully deleted */ @@ -399,6 +509,119 @@ final class ManagingProductVariantsContext implements Context ); } + /** + * @Then inventory of this variant should not be tracked + */ + public function inventoryOfThisVariantShouldNotBeTracked(): void + { + Assert::true($this->responseChecker->hasValue($this->client->getLastResponse(), 'tracked', false)); + } + + /** + * @Then inventory of this variant should be tracked + */ + public function inventoryOfThisVariantShouldBeTracked(): void + { + Assert::true($this->responseChecker->hasValue($this->client->getLastResponse(), 'tracked', true)); + } + + /** + * @Then I should be notified that prices in all channels must be defined + */ + public function iShouldBeNotifiedThatPricesInAllChannelsMustBeDefined(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'You must define price for every enabled channel.', + ); + } + + /** + * @Then I should be notified that price cannot be lower than 0 + */ + public function iShouldBeNotifiedThatPriceCannotBeLowerThanZero(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Price cannot be lower than 0.', + ); + } + + /** + * @Then I should be notified that code is required + */ + public function iShouldBeNotifiedThatCodeIsRequired(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Please enter the code.', + ); + } + + /** + * @Then I should be notified that current stock is required + */ + public function iShouldBeNotifiedThatCurrentStockIsRequired(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'The type of the "onHand" attribute must be "int", "NULL" given.', + ); + } + + /** + * @Then the :product product should have no variants + */ + public function theProductShouldHaveNoVariants(ProductInterface $product): void + { + $this->iWantToViewAllVariantsOfThisProduct($product); + $this->iShouldSeeNumberOfProductVariantsInTheList(0); + } + + /** + * @Then the :product product should have only one variant + */ + public function theProductShouldHaveOnlyOneVariant(ProductInterface $product): void + { + $this->iWantToViewAllVariantsOfThisProduct($product); + $this->iShouldSeeNumberOfProductVariantsInTheList(1); + } + + /** + * @Then I should be notified that code has to be unique + */ + public function iShouldBeNotifiedThatCodeHasToBeUnique(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Product variant code must be unique.', + ); + } + + /** + * @Then I should be notified that this variant already exists + */ + public function iShouldBeNotifiedThatThisVariantAlreadyExists(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Variant with this option set already exists.', + ); + } + + /** + * @Then I should be notified that height, width, depth and weight cannot be lower than 0 + */ + public function iShouldBeNotifiedThatIsHeightWidthDepthAndWeightCannotBeLowerThanZero(): void + { + $errors = $this->responseChecker->getError($this->client->getLastResponse()); + + Assert::contains($errors, 'Height cannot be negative.'); + Assert::contains($errors, 'Width cannot be negative.'); + Assert::contains($errors, 'Depth cannot be negative.'); + Assert::contains($errors, 'Weight cannot be negative.'); + } + private function updateChannelPricingField( ProductVariantInterface $variant, ChannelInterface $channel, diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php index de8814cf25..877b5c10ae 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php @@ -107,6 +107,14 @@ final class ManagingProductVariantsContext implements Context $this->createPage->specifyPrice($price ?? '', $channel ?? $this->sharedStorage->get('channel')); } + /** + * @When /^I set its price to "-(?:€|£|\$)([^"]+)" for ("([^"]+)" channel)$/ + */ + public function iSetItsNegativePriceTo(string $price, ChannelInterface $channel): void + { + $this->createPage->specifyPrice('-' . $price , $channel); + } + /** * @When /^I set its minimum price to "(?:€|£|\$)([^"]+)" for ("([^"]+)" channel)$/ */ @@ -116,7 +124,7 @@ final class ManagingProductVariantsContext implements Context } /** - * @When I remove its price for :channel channel + * @When I remove its price from :channel channel */ public function iRemoveItsPriceForChannel(ChannelInterface $channel): void { diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductsContext.php index ece76de255..9aa4110b60 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductsContext.php @@ -1080,7 +1080,7 @@ final class ManagingProductsContext implements Context } /** - * @When /^I remove its price for ("[^"]+" channel)$/ + * @When /^I remove its price from ("[^"]+" channel)$/ */ public function iRemoveItsPriceForChannel(ChannelInterface $channel): void { From 3fddabca2d6ee6bcbeb5dbccb7e8093ee7f0e3dc Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Mon, 23 Oct 2023 10:36:05 +0200 Subject: [PATCH 10/14] [API] Validate option values on product variants --- .../editing_product_variant.feature | 14 ++- ...s_from_options_without_any_values.feature} | 0 .../product_variant_validation.feature | 12 ++ ...lues_while_editing_product_variant.feature | 4 +- .../Admin/ManagingProductVariantsContext.php | 68 +++++++++++- .../Transform/ProductOptionValueContext.php | 1 + .../Admin/ManagingProductVariantsContext.php | 13 +++ .../config/services/contexts/api/admin.xml | 1 + .../api/product/managing_product_variants.yml | 1 + .../ui/product/managing_product_variants.yml | 1 + .../Resources/config/services/validator.xml | 4 + .../config/validation/ProductVariant.xml | 25 +++++ .../Resources/translations/validators.en.yaml | 2 + .../SingleValueForProductVariantOption.php | 32 ++++++ ...eValueForProductVariantOptionValidator.php | 40 +++++++ ...ueForProductVariantOptionValidatorSpec.php | 103 ++++++++++++++++++ 16 files changed, 317 insertions(+), 4 deletions(-) rename features/product/managing_product_variants/{preventing_the_generation_of_products_variants_from_options_without_any_values.feature => preventing_generation_of_products_variants_from_options_without_any_values.feature} (100%) create mode 100644 src/Sylius/Bundle/ApiBundle/Resources/config/validation/ProductVariant.xml create mode 100644 src/Sylius/Bundle/ApiBundle/Validator/Constraints/SingleValueForProductVariantOption.php create mode 100644 src/Sylius/Bundle/ApiBundle/Validator/Constraints/SingleValueForProductVariantOptionValidator.php create mode 100644 src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/SingleValueForProductVariantOptionValidatorSpec.php diff --git a/features/product/managing_product_variants/editing_product_variant.feature b/features/product/managing_product_variants/editing_product_variant.feature index 81575bc11f..67b59c80b1 100644 --- a/features/product/managing_product_variants/editing_product_variant.feature +++ b/features/product/managing_product_variants/editing_product_variant.feature @@ -8,7 +8,9 @@ Feature: Editing a product variant Given the store operates on a single channel in "United States" And this channel allows to shop using "English (United States)" and "Polish (Poland)" locales And the store has a "T-Shirt" configurable product - And this product has "Go" variant priced at "$100.00" in "United States" channel + And this product has option "Size" with values "S", "M" and "L" + And this product has option "Color" with values "Green" and "Blue" + And this product has "Go" variant priced at "$100.00" configured with "S" option value And this product is named "Go" in the "English (United States)" locale And this product is named "Idź" in the "Polish (Poland)" locale And I am logged in as an administrator @@ -22,3 +24,13 @@ Feature: Editing a product variant Then I should be notified that it has been successfully edited And the variant with code "GO" should be named "Java" in "English (United States)" locale And the variant with code "GO" should be named "Kawa" in "Polish (Poland)" locale + + @api @ui + Scenario: Changing product variant option values + When I want to modify the "Go" product variant + And I set its "Color" option to "Green" + And I change its "Size" option to "L" + And I save my changes + Then I should be notified that it has been successfully edited + And the variant "Go" should have "Color" option as "Green" + And the variant "Go" should have "Size" option as "L" diff --git a/features/product/managing_product_variants/preventing_the_generation_of_products_variants_from_options_without_any_values.feature b/features/product/managing_product_variants/preventing_generation_of_products_variants_from_options_without_any_values.feature similarity index 100% rename from features/product/managing_product_variants/preventing_the_generation_of_products_variants_from_options_without_any_values.feature rename to features/product/managing_product_variants/preventing_generation_of_products_variants_from_options_without_any_values.feature diff --git a/features/product/managing_product_variants/product_variant_validation.feature b/features/product/managing_product_variants/product_variant_validation.feature index db1d21147e..4459dba702 100644 --- a/features/product/managing_product_variants/product_variant_validation.feature +++ b/features/product/managing_product_variants/product_variant_validation.feature @@ -58,6 +58,18 @@ Feature: Product variant validation Then I should be notified that this variant already exists And the "Wyborowa Vodka" product should have only one variant + @api @no-ui + Scenario: Adding a new product variant with two values of the same option + Given this product has option "Taste" with values "Orange" and "Melon" + When I want to create a new variant of this product + And I specify its code as "VODKA_WYBOROWA_PREMIUM" + And I set its "Taste" option to "Orange" + And I set its "Taste" option to "Melon" + And I set its price to "$100.00" for "United States" channel + And I try to add it + Then I should be notified that the variant can have only one value configured for a single option + And the "Wyborowa Vodka" product should have no variants + @api @ui Scenario: Adding a new product variant with negative properties When I want to create a new variant of this product diff --git a/features/product/managing_product_variants/seeing_correct_option_values_while_editing_product_variant.feature b/features/product/managing_product_variants/seeing_correct_option_values_while_editing_product_variant.feature index 993d907c57..bb88bcc446 100644 --- a/features/product/managing_product_variants/seeing_correct_option_values_while_editing_product_variant.feature +++ b/features/product/managing_product_variants/seeing_correct_option_values_while_editing_product_variant.feature @@ -12,13 +12,13 @@ Feature: Seeing correct option values while editing product variant And this product has option "Type" with values "Clear" and "Color" And I am logged in as an administrator - @ui + @ui @no-api Scenario: Seeing default option values while editing product variant in store When I want to modify the "Wyborowa Vodka Exquisite" product variant And I should see the "Type" option as "Clear" And I should see the "Taste" option as "Orange" - @ui + @ui @no-api Scenario: Seeing changed option values while editing product variant in store When I want to modify the "Wyborowa Vodka Exquisite" product variant And I change its "Taste" option to "Melon" diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php index 43505047a3..25db6e9f00 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php @@ -18,10 +18,12 @@ use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; +use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface; use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\ProductVariantInterface; +use Sylius\Component\Product\Model\ProductOptionInterface; use Sylius\Component\Product\Model\ProductOptionValueInterface; use Sylius\Component\Shipping\Model\ShippingCategoryInterface; use Webmozart\Assert\Assert; @@ -34,6 +36,7 @@ final class ManagingProductVariantsContext implements Context private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private IriConverterInterface $iriConverter, + private SectionAwareIriConverterInterface $sectionAwareIriConverter, ) { } @@ -210,7 +213,44 @@ final class ManagingProductVariantsContext implements Context */ public function iSetItsOptionAs(string $optionName, ProductOptionValueInterface $optionValue): void { - $this->client->addRequestData('optionValues', [$this->iriConverter->getIriFromResource($optionValue)]); + $content = $this->client->getContent(); + $content['optionValues'][] = $this->sectionAwareIriConverter->getIriFromResourceInSection($optionValue, 'admin'); + + $this->client->setRequestData($content); + } + + /** + * @When I change its :productOption option to :productOptionValue + */ + public function iChangeItsOptionTo( + ProductOptionInterface $productOption, + ProductOptionValueInterface $productOptionValue, + ): void { + $content = $this->client->getContent(); + foreach ($content['optionValues'] as $key => $optionValueIri) { + /** @var ProductOptionValueInterface $currentOptionValue */ + $currentOptionValue = $this->iriConverter->getResourceFromIri($optionValueIri); + if ($currentOptionValue->getOptionCode() === $productOption->getCode()) { + unset($content['optionValues'][$key]); + } + } + + $content['optionValues'][] = $this->iriConverter->getIriFromResource($productOptionValue); + + $this->client->setRequestData($content); + } + + /** + * @When I add additionally :productOptionValue value as :productOptionName option + */ + public function iAddAdditionallyValueAsOption( + ProductOptionValueInterface $productOptionValue, + string $productOptionName, + ): void { + $content = $this->client->getContent(); + $content['optionValues'][] = $this->iriConverter->getIriFromResource($productOptionValue); + + $this->client->setRequestData($content); } /** @@ -622,6 +662,32 @@ final class ManagingProductVariantsContext implements Context Assert::contains($errors, 'Weight cannot be negative.'); } + /** + * @Then the variant :productVariantName should have :optionName option as :optionValue + */ + public function theVariantShouldHaveOptionAs( + string $productVariantName, + string $optionName, + ProductOptionValueInterface $optionValue + ): void { + Assert::true($this->responseChecker->hasValueInCollection( + $this->client->getLastResponse(), + 'optionValues', + $this->sectionAwareIriConverter->getIriFromResourceInSection($optionValue, 'admin'), + )); + } + + /** + * @Then I should be notified that the variant can have only one value configured for a single option + */ + public function iShouldBeNotifiedThatTheVariantCanHaveOnlyOneValueConfiguredForASingleOption(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'The product variant can have only one value configured for a single option.', + ); + } + private function updateChannelPricingField( ProductVariantInterface $variant, ChannelInterface $channel, diff --git a/src/Sylius/Behat/Context/Transform/ProductOptionValueContext.php b/src/Sylius/Behat/Context/Transform/ProductOptionValueContext.php index 6711f2aa47..493b43723a 100644 --- a/src/Sylius/Behat/Context/Transform/ProductOptionValueContext.php +++ b/src/Sylius/Behat/Context/Transform/ProductOptionValueContext.php @@ -27,6 +27,7 @@ final class ProductOptionValueContext implements Context /** * @Transform /^"([^"]+)" option value$/ * @Transform :optionValue + * @Transform :productOptionValue */ public function getProductOptionValueByCode(string $code): ProductOptionValueInterface { diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php index 877b5c10ae..23dfd0ef68 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php @@ -681,6 +681,19 @@ final class ManagingProductVariantsContext implements Context Assert::same($this->indexPage->countItemsWithNoName(), $count); } + /** + * @Then the variant :productVariant should have :optionName option as :optionValue + */ + public function theVariantShouldHaveOptionAs( + ProductVariantInterface $productVariant, + string $optionName, + string $optionValue + ): void { + $this->updatePage->open(['id' => $productVariant->getId(), 'productId' => $productVariant->getProduct()->getId()]); + + Assert::true($this->updatePage->isSelectedOptionValueOnPage($optionName, $optionValue)); + } + /** * @param string $element * @param string $message diff --git a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml index 66484e80ed..dc16986594 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml @@ -115,6 +115,7 @@ + diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_variants.yml b/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_variants.yml index 7b72ffa96b..bd153ab63f 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_variants.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_variants.yml @@ -12,6 +12,7 @@ default: - sylius.behat.context.transform.locale - sylius.behat.context.transform.lexical - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_option - sylius.behat.context.transform.product_option_value - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shared_storage diff --git a/src/Sylius/Behat/Resources/config/suites/ui/product/managing_product_variants.yml b/src/Sylius/Behat/Resources/config/suites/ui/product/managing_product_variants.yml index 1113bad570..e09d123748 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/product/managing_product_variants.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/product/managing_product_variants.yml @@ -13,6 +13,7 @@ default: - sylius.behat.context.transform.locale - sylius.behat.context.transform.lexical - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_option_value - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.channel diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/validator.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/validator.xml index 119f239121..180c04d84c 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/validator.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/validator.xml @@ -131,6 +131,10 @@ + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ProductVariant.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ProductVariant.xml new file mode 100644 index 0000000000..395e13d3db --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ProductVariant.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/translations/validators.en.yaml b/src/Sylius/Bundle/ApiBundle/Resources/translations/validators.en.yaml index 8d86a275e3..d6dc2b4b6c 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/translations/validators.en.yaml +++ b/src/Sylius/Bundle/ApiBundle/Resources/translations/validators.en.yaml @@ -23,6 +23,8 @@ sylius: not_longer_available: 'The product variant with name %productVariantName% does not exist.' not_sufficient: 'The product variant with %productVariantCode% code does not have sufficient stock.' product_variant_with_name_not_sufficient: 'The product variant with %productVariantName% name does not have sufficient stock.' + option_values: + single_value: 'The product variant can have only one value configured for a single option.' reset_password: invalid_token: 'Password reset token %token% is invalid.' token_expired: 'Password reset token has expired.' diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/SingleValueForProductVariantOption.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/SingleValueForProductVariantOption.php new file mode 100644 index 0000000000..93748f6db2 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/SingleValueForProductVariantOption.php @@ -0,0 +1,32 @@ + $productOptionValue->getOptionCode(), $value->getOptionValues()->toArray()); + /** @psalm-var array */ + $flippedMap = array_flip($map); + if (count($map) !== count($flippedMap)) { + $this->context->addViolation($constraint->message); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/SingleValueForProductVariantOptionValidatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/SingleValueForProductVariantOptionValidatorSpec.php new file mode 100644 index 0000000000..122904e1d0 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/SingleValueForProductVariantOptionValidatorSpec.php @@ -0,0 +1,103 @@ +initialize($executionContext); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldImplement(ConstraintValidatorInterface::class); + } + + function it_throws_an_exception_if_value_is_not_a_product_variant( + ExecutionContextInterface $context, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [new \stdClass(), new SingleValueForProductVariantOption()]) + ; + } + + function it_throws_an_exception_if_constraint_is_not_a_single_value_for_product_variant_option( + ExecutionContextInterface $context, + ProductVariantInterface $variant, + Constraint $constraint, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [$variant, $constraint]) + ; + } + + function it_adds_violation_if_there_is_more_than_one_option_value_to_a_single_option( + ExecutionContextInterface $executionContext, + ProductVariantInterface $variant, + ProductOptionValueInterface $firstProductOptionValue, + ProductOptionValueInterface $secondProductOptionValue, + ): void { + $constraint = new SingleValueForProductVariantOption(); + + $firstProductOptionValue->getOptionCode()->willReturn('OPTION'); + $secondProductOptionValue->getOptionCode()->willReturn('OPTION'); + + $variant->getOptionValues()->willReturn(new ArrayCollection([ + $firstProductOptionValue->getWrappedObject(), + $secondProductOptionValue->getWrappedObject(), + ])); + + $executionContext->addViolation('sylius.product_variant.option_values.single_value')->shouldBeCalled(); + + $this->validate($variant, $constraint); + } + + function it_does_nothing_if_each_option_has_only_one_option_value( + ExecutionContextInterface $executionContext, + ProductVariantInterface $variant, + ProductOptionValueInterface $firstProductOptionValue, + ProductOptionValueInterface $secondProductOptionValue, + ): void { + $constraint = new SingleValueForProductVariantOption(); + + $firstProductOptionValue->getOptionCode()->willReturn('OPTION'); + $secondProductOptionValue->getOptionCode()->willReturn('DIFFERENT_OPTION'); + + $variant->getOptionValues()->willReturn(new ArrayCollection([ + $firstProductOptionValue->getWrappedObject(), + $secondProductOptionValue->getWrappedObject(), + ])); + + $executionContext->addViolation($constraint->message)->shouldNotBeCalled(); + + $this->validate($variant, $constraint); + } +} From 6df65d3842f7c79bca569e0260631f7b4b353327 Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Tue, 24 Oct 2023 09:23:18 +0200 Subject: [PATCH 11/14] Fix CS and static analysis --- phpstan-baseline.neon | 10 ++++++++++ .../Api/Admin/ManagingProductVariantsContext.php | 10 +++++++--- .../Ui/Admin/ManagingProductVariantsContext.php | 4 ++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9b68f1b598..01be94cb52 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -810,6 +810,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductTaxonDataPersister.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductVariantDataPersister\\:\\:remove\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductVariantDataPersister.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ShippingMethodDataPersister\\:\\:persist\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -2090,6 +2095,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserNotVerifiedValidator.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\SingleValueForProductVariantOptionValidator\\:\\:validate\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/SingleValueForProductVariantOptionValidator.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\UniqueReviewerEmailValidator\\:\\:__construct\\(\\) has parameter \\$shopUserRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" count: 1 diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php index 25db6e9f00..0c21c451e8 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php @@ -519,7 +519,9 @@ final class ManagingProductVariantsContext implements Context { Assert::false( $this->responseChecker->hasItemWithValue( - $this->client->index(Resources::PRODUCT_VARIANTS), 'code', $productVariant->getCode(), + $this->client->index(Resources::PRODUCT_VARIANTS), + 'code', + $productVariant->getCode(), ), 'The product variant still exists, but it should not', ); @@ -532,7 +534,9 @@ final class ManagingProductVariantsContext implements Context { Assert::true( $this->responseChecker->hasItemWithValue( - $this->client->index(Resources::PRODUCT_VARIANTS), 'code', $productVariant->getCode(), + $this->client->index(Resources::PRODUCT_VARIANTS), + 'code', + $productVariant->getCode(), ), 'The product variant does not exist, but it should', ); @@ -668,7 +672,7 @@ final class ManagingProductVariantsContext implements Context public function theVariantShouldHaveOptionAs( string $productVariantName, string $optionName, - ProductOptionValueInterface $optionValue + ProductOptionValueInterface $optionValue, ): void { Assert::true($this->responseChecker->hasValueInCollection( $this->client->getLastResponse(), diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php index 23dfd0ef68..513a504c37 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php @@ -112,7 +112,7 @@ final class ManagingProductVariantsContext implements Context */ public function iSetItsNegativePriceTo(string $price, ChannelInterface $channel): void { - $this->createPage->specifyPrice('-' . $price , $channel); + $this->createPage->specifyPrice('-' . $price, $channel); } /** @@ -687,7 +687,7 @@ final class ManagingProductVariantsContext implements Context public function theVariantShouldHaveOptionAs( ProductVariantInterface $productVariant, string $optionName, - string $optionValue + string $optionValue, ): void { $this->updatePage->open(['id' => $productVariant->getId(), 'productId' => $productVariant->getProduct()->getId()]); From 89c0bf58ae3f744bf06da38eaac16a5c26d7000d Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Tue, 24 Oct 2023 09:59:40 +0200 Subject: [PATCH 12/14] [API] Fix contract tests after changing fixtures for product variants --- tests/Api/Admin/CatalogPromotionsTest.php | 33 ++++++++++++++++--- .../get_product_variants_response.json | 14 +++++++- tests/Api/Shop/ProductReviewsTest.php | 21 ++++++++++-- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/tests/Api/Admin/CatalogPromotionsTest.php b/tests/Api/Admin/CatalogPromotionsTest.php index e84cc066dd..549de8a532 100644 --- a/tests/Api/Admin/CatalogPromotionsTest.php +++ b/tests/Api/Admin/CatalogPromotionsTest.php @@ -68,7 +68,13 @@ final class CatalogPromotionsTest extends JsonApiTestCase /** @test */ public function it_creates_a_catalog_promotion(): void { - $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml', 'product/product_variant.yaml']); + $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); $this->client->request( @@ -190,7 +196,13 @@ final class CatalogPromotionsTest extends JsonApiTestCase /** @test */ public function it_does_not_create_a_catalog_promotion_with_invalid_scopes(): void { - $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml', 'product/product_variant.yaml']); + $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); $this->client->request( @@ -278,7 +290,13 @@ final class CatalogPromotionsTest extends JsonApiTestCase /** @test */ public function it_does_not_create_a_catalog_promotion_with_invalid_actions(): void { - $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml', 'product/product_variant.yaml']); + $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); $this->client->request( @@ -421,7 +439,14 @@ final class CatalogPromotionsTest extends JsonApiTestCase private function loadFixturesAndGetCatalogPromotion(): CatalogPromotionInterface { - $fixtures = $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml', 'product/product_variant.yaml', 'catalog_promotion.yaml']); + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + 'catalog_promotion.yaml', + ]); /** @var CatalogPromotionInterface $catalogPromotion */ $catalogPromotion = $fixtures['catalog_promotion']; diff --git a/tests/Api/Responses/Expected/admin/product_variant/get_product_variants_response.json b/tests/Api/Responses/Expected/admin/product_variant/get_product_variants_response.json index 8b55d7452f..b087008948 100644 --- a/tests/Api/Responses/Expected/admin/product_variant/get_product_variants_response.json +++ b/tests/Api/Responses/Expected/admin/product_variant/get_product_variants_response.json @@ -91,9 +91,21 @@ "hydra:totalItems": 2, "hydra:search": { "@type": "hydra:IriTemplate", - "hydra:template": "\/api\/v2\/admin\/product-variants{?catalogPromotion}", + "hydra:template": "\/api\/v2\/admin\/product-variants{?product,product[],catalogPromotion}", "hydra:variableRepresentation": "BasicRepresentation", "hydra:mapping": [ + { + "@type": "IriTemplateMapping", + "variable": "product", + "property": "product", + "required": false + }, + { + "@type": "IriTemplateMapping", + "variable": "product[]", + "property": "product", + "required": false + }, { "@type": "IriTemplateMapping", "variable": "catalogPromotion", diff --git a/tests/Api/Shop/ProductReviewsTest.php b/tests/Api/Shop/ProductReviewsTest.php index afac1a7eff..8f82c31af1 100644 --- a/tests/Api/Shop/ProductReviewsTest.php +++ b/tests/Api/Shop/ProductReviewsTest.php @@ -57,7 +57,12 @@ final class ProductReviewsTest extends JsonApiTestCase /** @test */ public function it_creates_a_product_review(): void { - $fixtures = $this->loadFixturesFromFiles(['product/product_variant.yaml']); + $fixtures = $this->loadFixturesFromFiles([ + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); /** @var ProductInterface $product */ $product = $fixtures['product']; @@ -84,7 +89,12 @@ final class ProductReviewsTest extends JsonApiTestCase /** @test */ public function it_prevents_from_creating_a_product_review_with_non_existing_product(): void { - $this->loadFixturesFromFiles(['product/product_variant.yaml']); + $this->loadFixturesFromFiles([ + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); $this->client->request( method: 'POST', @@ -105,7 +115,12 @@ final class ProductReviewsTest extends JsonApiTestCase /** @test */ public function it_prevents_from_creating_a_product_review_if_no_product_provided(): void { - $this->loadFixturesFromFiles(['product/product_variant.yaml']); + $this->loadFixturesFromFiles([ + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); $this->client->request( method: 'POST', From 23eb3adb885d1a0f36f0594bcf8d0ab6982792b7 Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Tue, 24 Oct 2023 14:25:25 +0200 Subject: [PATCH 13/14] [Behat][API] Extract some steps to separate contexts to resolve duplications --- .../Admin/CreatingProductVariantContext.php | 52 ++++++++++++ .../Admin/ManagingProductVariantsContext.php | 80 ------------------- .../ManagingProductVariantsPricesContext.php | 74 +++++++++++++++++ .../config/services/contexts/api/admin.xml | 9 +++ .../promotion/applying_catalog_promotions.yml | 3 +- 5 files changed, 137 insertions(+), 81 deletions(-) create mode 100644 src/Sylius/Behat/Context/Api/Admin/CreatingProductVariantContext.php create mode 100644 src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsPricesContext.php diff --git a/src/Sylius/Behat/Context/Api/Admin/CreatingProductVariantContext.php b/src/Sylius/Behat/Context/Api/Admin/CreatingProductVariantContext.php new file mode 100644 index 0000000000..423803d6ad --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/CreatingProductVariantContext.php @@ -0,0 +1,52 @@ +client->buildCreateRequest(Resources::PRODUCT_VARIANTS); + $this->client->addRequestData('product', $this->iriConverter->getIriFromResource($product)); + $this->client->addRequestData('code', StringInflector::nameToCode($name)); + + $this->client->addRequestData('channelPricings', [ + $channel->getCode() => [ + 'price' => $price, + 'channelCode' => $channel->getCode(), + ], + ]); + + $this->client->create(); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php index 0c21c451e8..de98b36139 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php @@ -19,7 +19,6 @@ use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface; -use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\ProductVariantInterface; @@ -143,50 +142,6 @@ final class ManagingProductVariantsContext implements Context $this->client->create(); } - /** - * @When /^I change the price of the ("[^"]+" product variant) to ("[^"]+") in ("[^"]+" channel)$/ - */ - public function iChangeThePriceOfTheProductVariantInChannel( - ProductVariantInterface $variant, - int $price, - ChannelInterface $channel, - ): void { - $this->updateChannelPricingField($variant, $channel, $price, 'price'); - } - - /** - * @When /^I change the original price of the ("[^"]+" product variant) to ("[^"]+") in ("[^"]+" channel)$/ - */ - public function iChangeTheOriginalPriceOfTheProductVariantInChannel( - ProductVariantInterface $variant, - int $originalPrice, - ChannelInterface $channel, - ): void { - $this->updateChannelPricingField($variant, $channel, $originalPrice, 'originalPrice'); - } - - /** - * @When /^I remove the original price of the ("[^"]+" product variant) in ("[^"]+" channel)$/ - */ - public function iRemoveTheOriginalPriceOfTheProductVariantInChannel( - ProductVariantInterface $variant, - ChannelInterface $channel, - ): void { - $this->updateChannelPricingField($variant, $channel, null, 'originalPrice'); - } - - /** - * @When /^I create a new "([^"]+)" variant priced at ("[^"]+") for ("[^"]+" product) in the ("[^"]+" channel)$/ - */ - public function iCreateANewVariantPricedAtForProductInTheChannel( - string $name, - int $price, - ProductInterface $product, - ChannelInterface $channel, - ): void { - $this->createNewVariantWithPrice($name, $price, $product, $channel); - } - /** * @When I want to modify the :variant product variant */ @@ -691,39 +646,4 @@ final class ManagingProductVariantsContext implements Context 'The product variant can have only one value configured for a single option.', ); } - - private function updateChannelPricingField( - ProductVariantInterface $variant, - ChannelInterface $channel, - ?int $price, - string $field, - ): void { - $this->client->buildUpdateRequest(Resources::PRODUCT_VARIANTS, $variant->getCode()); - - $content = $this->client->getContent(); - $content['channelPricings'][$channel->getCode()][$field] = $price; - $this->client->updateRequestData($content); - - $this->client->update(); - } - - private function createNewVariantWithPrice( - string $name, - int $price, - ProductInterface $product, - ChannelInterface $channel, - ): void { - $this->client->buildCreateRequest(Resources::PRODUCT_VARIANTS); - $this->client->addRequestData('product', $this->iriConverter->getIriFromResource($product)); - $this->client->addRequestData('code', StringInflector::nameToCode($name)); - - $this->client->addRequestData('channelPricings', [ - $channel->getCode() => [ - 'price' => $price, - 'channelCode' => $channel->getCode(), - ], - ]); - - $this->client->create(); - } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsPricesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsPricesContext.php new file mode 100644 index 0000000000..f5ddf9133e --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsPricesContext.php @@ -0,0 +1,74 @@ +updateChannelPricingField($variant, $channel, $price, 'price'); + } + + /** + * @When /^I change the original price of the ("[^"]+" product variant) to ("[^"]+") in ("[^"]+" channel)$/ + */ + public function iChangeTheOriginalPriceOfTheProductVariantInChannel( + ProductVariantInterface $variant, + int $originalPrice, + ChannelInterface $channel, + ): void { + $this->updateChannelPricingField($variant, $channel, $originalPrice, 'originalPrice'); + } + + /** + * @When /^I remove the original price of the ("[^"]+" product variant) in ("[^"]+" channel)$/ + */ + public function iRemoveTheOriginalPriceOfTheProductVariantInChannel( + ProductVariantInterface $variant, + ChannelInterface $channel, + ): void { + $this->updateChannelPricingField($variant, $channel, null, 'originalPrice'); + } + + private function updateChannelPricingField( + ProductVariantInterface $variant, + ChannelInterface $channel, + ?int $price, + string $field, + ): void { + $this->client->buildUpdateRequest(Resources::PRODUCT_VARIANTS, $variant->getCode()); + + $content = $this->client->getContent(); + $content['channelPricings'][$channel->getCode()][$field] = $price; + $this->client->updateRequestData($content); + + $this->client->update(); + } +} diff --git a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml index dc16986594..bf8fda640b 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml @@ -40,6 +40,11 @@ + + + + + @@ -118,6 +123,10 @@ + + + + diff --git a/src/Sylius/Behat/Resources/config/suites/api/promotion/applying_catalog_promotions.yml b/src/Sylius/Behat/Resources/config/suites/api/promotion/applying_catalog_promotions.yml index d9714118ce..eedb5128b8 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/promotion/applying_catalog_promotions.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/promotion/applying_catalog_promotions.yml @@ -25,11 +25,12 @@ default: - Sylius\Behat\Context\Transform\CatalogPromotionContext - sylius.behat.context.api.admin.managing_catalog_promotions - - sylius.behat.context.api.admin.managing_product_variants - sylius.behat.context.api.admin.save - sylius.behat.context.api.shop.login - sylius.behat.context.api.shop.product - sylius.behat.context.api.shop.product_variant + - Sylius\Behat\Context\Api\Admin\CreatingProductVariantContext + - Sylius\Behat\Context\Api\Admin\ManagingProductVariantsPricesContext - Sylius\Behat\Context\Api\Admin\ManagingProductTaxonsContext filters: From e99377eeedebdd91ec8f4356caa5637cb08adcb0 Mon Sep 17 00:00:00 2001 From: Grzegorz Sadowski Date: Tue, 24 Oct 2023 14:43:20 +0200 Subject: [PATCH 14/14] [Behat][API] Change setting code on option value to resolve problem with case sensitive PostgreSQL --- .../viewing_products/viewing_diagonal_variant_options.feature | 4 ++-- src/Sylius/Behat/Context/Setup/ProductContext.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/product/viewing_products/viewing_diagonal_variant_options.feature b/features/product/viewing_products/viewing_diagonal_variant_options.feature index bb2af1208e..0788dcfd4f 100644 --- a/features/product/viewing_products/viewing_diagonal_variant_options.feature +++ b/features/product/viewing_products/viewing_diagonal_variant_options.feature @@ -34,6 +34,6 @@ Feature: Viewing diagonal variants options @api @no-ui Scenario: Not seeing unavailable variants When I view variants of the "Extra Cool T-Shirt" product - And I filter them by "BLUE" option value - And I filter them by "SMALL" option value + And I filter them by "Blue" option value + And I filter them by "Small" option value Then I should not see any variants diff --git a/src/Sylius/Behat/Context/Setup/ProductContext.php b/src/Sylius/Behat/Context/Setup/ProductContext.php index 95f9fa07d3..d5eef3652d 100644 --- a/src/Sylius/Behat/Context/Setup/ProductContext.php +++ b/src/Sylius/Behat/Context/Setup/ProductContext.php @@ -1563,7 +1563,7 @@ final class ProductContext implements Context $this->sharedStorage->set(sprintf('%s_option', StringInflector::nameToLowercaseCode($optionName)), $option); foreach ($values as $value) { - $optionValue = $this->addProductOption($option, $value, StringInflector::nameToUppercaseCode($value)); + $optionValue = $this->addProductOption($option, $value, StringInflector::nameToCode($value)); $this->sharedStorage->set(sprintf('%s_option_%s_value', $value, strtolower($optionName)), $optionValue); }