feature #15460 [API][Admin] Managing product variants (GSadee)

This PR was merged into the 1.13 branch.

Discussion
----------

| Q               | A                                                            |
|-----------------|--------------------------------------------------------------|
| Branch?         | 1.13
| Bug fix?        | no
| New feature?    | yes                                                       |
| BC breaks?      | no
| Deprecations?   | no
| Related tickets | 
| License         | MIT                                                          |

<!--
 - Bug fixes must be submitted against the 1.12 branch
 - Features and deprecations must be submitted against the 1.13 branch
 - Make sure that the correct base branch is set

 To be sure you are not breaking any Backward Compatibilities, check the documentation:
 https://docs.sylius.com/en/latest/book/organization/backward-compatibility-promise.html
-->


Commits
-------
  [API][Admin] Extend GET endpoints for getting product variants
  [API][Admin] Extend POST endpoint for creating a product variant
  [API][Admin] Extend PUT endpoint for updating the product variant
  [API][Admin] Add DELETE endpoint for product variant resource
  [Behat][API] Cover scenarios for adding a product variant
  [Behat][API] Cover scenarios for browsing product variants
  [Behat][API] Cover scenarios for deleting a product variant
  [API][Behat] Mark scenarios for generating product variants as no-api
  [API][Behat] Cover scenarios for editing and validating product variants
  [API] Validate option values on product variants
  Fix CS and static analysis
  [API] Fix contract tests after changing fixtures for product variants
  [Behat][API] Extract some steps to separate contexts to resolve duplications
  [Behat][API] Change setting code on option value to resolve problem with case sensitive PostgreSQL
This commit is contained in:
Kamil Grygierzec 2023-10-31 16:07:35 +01:00 committed by GitHub
commit 9518e82a41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 1696 additions and 312 deletions

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -8,17 +8,29 @@ 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
@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
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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,19 @@ 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 @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
And I specify its code as "VODKA_WYBOROWA_PREMIUM"
@ -68,7 +80,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"

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -815,6 +815,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
@ -2095,6 +2100,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

View file

@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Behat\Context\Api\Admin;
use ApiPlatform\Api\IriConverterInterface;
use Behat\Behat\Context\Context;
use Sylius\Behat\Client\ApiClientInterface;
use Sylius\Behat\Context\Api\Resources;
use Sylius\Component\Core\Formatter\StringInflector;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\ProductInterface;
final class CreatingProductVariantContext implements Context
{
public function __construct(private ApiClientInterface $client, private IriConverterInterface $iriConverter)
{
}
/**
* @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->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();
}
}

View file

@ -18,10 +18,13 @@ use Behat\Behat\Context\Context;
use Sylius\Behat\Client\ApiClientInterface;
use Sylius\Behat\Client\ResponseCheckerInterface;
use Sylius\Behat\Context\Api\Resources;
use Sylius\Component\Core\Formatter\StringInflector;
use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface;
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;
final class ManagingProductVariantsContext implements Context
@ -32,6 +35,7 @@ final class ManagingProductVariantsContext implements Context
private ApiClientInterface $client,
private ResponseCheckerInterface $responseChecker,
private IriConverterInterface $iriConverter,
private SectionAwareIriConverterInterface $sectionAwareIriConverter,
) {
}
@ -78,6 +82,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)$/
*/
@ -103,57 +135,13 @@ final class ManagingProductVariantsContext implements Context
}
/**
* @When I add it
* @When I( try to) add it
*/
public function iAddIt(): void
{
$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
*/
@ -175,6 +163,131 @@ final class ManagingProductVariantsContext implements Context
]);
}
/**
* @When I set its :optionName option to :optionValue
*/
public function iSetItsOptionAs(string $optionName, ProductOptionValueInterface $optionValue): void
{
$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);
}
/**
* @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);
}
/**
* @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();
}
/**
* @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());
}
/**
* @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
*/
@ -190,15 +303,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)$/
*/
@ -221,15 +344,43 @@ 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 "[^"]+")$/
* @Then I should not have configured price for :channel channel
*/
public function theVariantWithCodeShouldBePricedAtForChannel(ProductVariantInterface $productVariant, int $price, ChannelInterface $channel): void
{
public function theVariantWithCodeShouldBePricedAtForChannel(
?string $variantCode = null,
?int $price = null,
?ChannelInterface $channel = null,
): 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 /^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 "([^"]+)")$/
*/
@ -240,38 +391,259 @@ final class ManagingProductVariantsContext implements Context
Assert::same($response[self::FIRST_COLLECTION_ITEM]['channelPricings'][$channel->getCode()]['minimumPrice'], $minimumPrice);
}
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();
/**
* @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 createNewVariantWithPrice(
string $name,
int $price,
ProductInterface $product,
ChannelInterface $channel,
/**
* @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],
));
}
/**
* @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
*/
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.',
);
}
/**
* @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.');
}
/**
* @Then the variant :productVariantName should have :optionName option as :optionValue
*/
public function theVariantShouldHaveOptionAs(
string $productVariantName,
string $optionName,
ProductOptionValueInterface $optionValue,
): void {
$this->client->buildCreateRequest(Resources::PRODUCT_VARIANTS);
$this->client->addRequestData('product', $this->iriConverter->getIriFromResource($product));
$this->client->addRequestData('code', StringInflector::nameToCode($name));
Assert::true($this->responseChecker->hasValueInCollection(
$this->client->getLastResponse(),
'optionValues',
$this->sectionAwareIriConverter->getIriFromResourceInSection($optionValue, 'admin'),
));
}
$this->client->addRequestData('channelPricings', [
$channel->getCode() => [
'price' => $price,
'channelCode' => $channel->getCode(),
],
]);
$this->client->create();
/**
* @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.',
);
}
}

View file

@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Behat\Context\Api\Admin;
use Behat\Behat\Context\Context;
use Sylius\Behat\Client\ApiClientInterface;
use Sylius\Behat\Context\Api\Resources;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\ProductVariantInterface;
final class ManagingProductVariantsPricesContext implements Context
{
public function __construct(private ApiClientInterface $client)
{
}
/**
* @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');
}
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();
}
}

View file

@ -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);
}

View file

@ -26,6 +26,8 @@ final class ProductOptionValueContext implements Context
/**
* @Transform /^"([^"]+)" option value$/
* @Transform :optionValue
* @Transform :productOptionValue
*/
public function getProductOptionValueByCode(string $code): ProductOptionValueInterface
{

View file

@ -71,6 +71,7 @@ final class ProductVariantContext implements Context
/**
* @Transform /^"([^"]+)" product variant$/
* @Transform /^"([^"]+)" variant$/
* @Transform :productVariant
* @Transform :variant
*/
public function getProductVariantByName($name)

View file

@ -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
{
@ -180,9 +188,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 +382,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 +406,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)
{
@ -671,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

View file

@ -1092,7 +1092,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
{

View file

@ -40,6 +40,11 @@
<argument type="service" id="api_platform.iri_converter" />
</service>
<service id="Sylius\Behat\Context\Api\Admin\CreatingProductVariantContext">
<argument type="service" id="sylius.behat.api_platform_client.admin" />
<argument type="service" id="api_platform.iri_converter" />
</service>
<service id="sylius.behat.context.api.admin.managing_administrators" class="Sylius\Behat\Context\Api\Admin\ManagingAdministratorsContext">
<argument type="service" id="sylius.behat.api_platform_client.admin" />
<argument type="service" id="Sylius\Behat\Client\ResponseCheckerInterface" />
@ -116,6 +121,11 @@
<argument type="service" id="sylius.behat.api_platform_client.admin" />
<argument type="service" id="Sylius\Behat\Client\ResponseCheckerInterface" />
<argument type="service" id="api_platform.iri_converter" />
<argument type="service" id="sylius.behat.section_iri_converter" />
</service>
<service id="Sylius\Behat\Context\Api\Admin\ManagingProductVariantsPricesContext">
<argument type="service" id="sylius.behat.api_platform_client.admin" />
</service>
<service id="sylius.behat.context.api.admin.managing_tax_categories" class="Sylius\Behat\Context\Api\Admin\ManagingTaxCategoriesContext">

View file

@ -12,8 +12,11 @@ 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
- sylius.behat.context.transform.shipping_category
- sylius.behat.context.transform.channel
- sylius.behat.context.transform.customer
- sylius.behat.context.transform.payment

View file

@ -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:

View file

@ -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

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Sylius\Bundle\ApiBundle\Exception\ProductVariantCannotBeRemoved;
use Sylius\Component\Core\Model\ProductVariantInterface;
/** @experimental */
final class ProductVariantDataPersister implements ContextAwareDataPersisterInterface
{
public function __construct(private ContextAwareDataPersisterInterface $decoratedDataPersister)
{
}
/** @param array<mixed> $context */
public function supports($data, array $context = []): bool
{
return $data instanceof ProductVariantInterface;
}
/** @param array<mixed> $context */
public function persist($data, array $context = [])
{
return $this->decoratedDataPersister->persist($data, $context);
}
/** @param array<mixed> $context */
public function remove($data, array $context = [])
{
try {
return $this->decoratedDataPersister->remove($data, $context);
} catch (ForeignKeyConstraintViolationException) {
throw new ProductVariantCannotBeRemoved();
}
}
}

View file

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Exception;
/** @experimental */
final class ProductVariantCannotBeRemoved extends \RuntimeException
{
public function __construct(
string $message = 'Cannot delete, the product variant is in use.',
int $code = 0,
\Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
}
}

View file

@ -45,6 +45,11 @@
<attribute name="groups">shop:product_variant:read</attribute>
</attribute>
</itemOperation>
<itemOperation name="admin_delete">
<attribute name="method">DELETE</attribute>
<attribute name="path">/admin/product-variants/{code}</attribute>
</itemOperation>
</itemOperations>
<collectionOperations>
@ -52,6 +57,7 @@
<attribute name="method">GET</attribute>
<attribute name="path">/admin/product-variants</attribute>
<attribute name="filters">
<attribute>sylius.api.product_variant_product_filter</attribute>
<attribute>Sylius\Bundle\ApiBundle\Filter\Doctrine\ProductVariantCatalogPromotionFilter</attribute>
</attribute>
<attribute name="normalization_context">

View file

@ -55,6 +55,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

View file

@ -23,20 +23,20 @@
<attribute name="price">
<group>admin:product_variant:read</group>
<group>admin:product_variant:update</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="originalPrice">
<group>admin:product_variant:read</group>
<group>admin:product_variant:update</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="minimumPrice">
<group>admin:product_variant:read</group>
<group>admin:product_variant:update</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="lowestPriceBeforeDiscount">

View file

@ -27,17 +27,19 @@
<group>shop:product_variant:read</group>
</attribute>
<attribute name="translations">
<group>admin:product_variant:create</group>
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="optionValues">
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
<group>shop:product_variant:read</group>
</attribute>
<attribute name="channelPricings">
<group>admin:product_variant:create</group>
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="name">
@ -48,5 +50,66 @@
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="position">
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="tracked">
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="onHold">
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="onHand">
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="weight">
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="width">
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="height">
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="depth">
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="taxCategory">
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="shippingCategory">
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="shippingRequired">
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
</attribute>
<attribute name="createdAt">
<group>admin:product_variant:read</group>
</attribute>
<attribute name="updatedAt">
<group>admin:product_variant:read</group>
</attribute>
</class>
</serializer>

View file

@ -17,25 +17,26 @@
>
<class name="Sylius\Component\Product\Model\ProductVariantTranslation">
<attribute name="id">
<group>shop:order:account:read</group>
<group>admin:product_variant:read</group>
<group>admin:order:read</group>
<group>shop:product_variant:read</group>
<group>shop:order:account:read</group>
</attribute>
<attribute name="name">
<group>shop:order:account:read</group>
<group>admin:order:read</group>
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
<group>admin:product_variant:read</group>
<group>admin:order:read</group>
<group>shop:product_variant:read</group>
<group>shop:order:account:read</group>
</attribute>
<attribute name="locale">
<group>shop:order:account:read</group>
<group>admin:order:read</group>
<group>admin:product_variant:read</group>
<group>admin:product_variant:create</group>
<group>admin:product_variant:update</group>
<group>admin:product_variant:read</group>
<group>admin:order:read</group>
<group>shop:product_variant:read</group>
<group>shop:order:account:read</group>
</attribute>
</class>
</serializer>

View file

@ -69,6 +69,11 @@
<tag name="api_platform.data_persister" />
</service>
<service id="Sylius\Bundle\ApiBundle\DataPersister\ProductVariantDataPersister">
<argument type="service" id="api_platform.doctrine.orm.data_persister" />
<tag name="api_platform.data_persister" />
</service>
<service id="Sylius\Bundle\ApiBundle\DataPersister\ZoneDataPersister">
<argument type="service" id="api_platform.doctrine.orm.data_persister" />
<argument type="service" id="Sylius\Component\Addressing\Checker\ZoneDeletionCheckerInterface" />

View file

@ -131,6 +131,10 @@
<tag name="validator.constraint_validator" alias="sylius_api_shop_user_not_verified" />
</service>
<service id="Sylius\Bundle\ApiBundle\Validator\Constraints\SingleValueForProductVariantOptionValidator">
<tag name="validator.constraint_validator" alias="sylius_api_validator_single_value_for_product_variant_option" />
</service>
<service id="Sylius\Bundle\ApiBundle\Validator\Constraints\AccountVerificationTokenEligibilityValidator">
<argument type="service" id="sylius.repository.shop_user" />
<tag name="validator.constraint_validator" alias="sylius_api_validator_account_verification_token_eligibility" />

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This file is part of the Sylius package.
(c) Sylius Sp. z o.o.
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
-->
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/services/constraint-mapping-1.0.xsd">
<class name="Sylius\Component\Core\Model\ProductVariant">
<constraint name="Sylius\Bundle\ApiBundle\Validator\Constraints\SingleValueForProductVariantOption">
<option name="groups">
<value>sylius</value>
</option>
</constraint>
</class>
</constraint-mapping>

View file

@ -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.'

View file

@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/** @experimental */
final class SingleValueForProductVariantOption extends Constraint
{
public string $message = 'sylius.product_variant.option_values.single_value';
public function validatedBy(): string
{
return 'sylius_api_validator_single_value_for_product_variant_option';
}
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}

View file

@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
use Sylius\Component\Core\Model\ProductVariantInterface;
use Sylius\Component\Product\Model\ProductOptionValueInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Webmozart\Assert\Assert;
/** @experimental */
final class SingleValueForProductVariantOptionValidator extends ConstraintValidator
{
public function validate(mixed $value, Constraint $constraint)
{
/** @var ProductVariantInterface $value */
Assert::isInstanceOf($value, ProductVariantInterface::class);
/** @var SingleValueForProductVariantOption $constraint */
Assert::isInstanceOf($constraint, SingleValueForProductVariantOption::class);
$map = array_map(fn (ProductOptionValueInterface $productOptionValue) => $productOptionValue->getOptionCode(), $value->getOptionValues()->toArray());
/** @psalm-var array<string, int> */
$flippedMap = array_flip($map);
if (count($map) !== count($flippedMap)) {
$this->context->addViolation($constraint->message);
}
}
}

View file

@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Exception\ProductVariantCannotBeRemoved;
use Sylius\Component\Core\Model\ProductVariantInterface;
final class ProductVariantDataPersisterSpec extends ObjectBehavior
{
function let(ContextAwareDataPersisterInterface $dataPersister): void
{
$this->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);
}
}

View file

@ -0,0 +1,103 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\Validator\Constraints;
use Doctrine\Common\Collections\ArrayCollection;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Bundle\ApiBundle\Validator\Constraints\SingleValueForProductVariantOption;
use Sylius\Component\Core\Model\ProductVariantInterface;
use Sylius\Component\Product\Model\ProductOptionValueInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
final class SingleValueForProductVariantOptionValidatorSpec extends ObjectBehavior
{
function let(ExecutionContextInterface $executionContext): void
{
$this->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);
}
}

View file

@ -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'];

View file

@ -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 */
@ -71,65 +87,25 @@ final class ProductVariantsTest extends JsonApiTestCase
}
/** @test */
public function it_updates_channel_pricing_and_translation_of_a_product_variant(): void
public function it_creates_a_product_variant_with_all_optional_data(): void
{
$fixtures = $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);
/** @var ProductVariantInterface $productVariant */
$productVariant = $fixtures['product_variant'];
/** @var ChannelInterface $channel */
$channel = $fixtures['channel_web'];
$this->client->request(
method: 'PUT',
uri: sprintf('/api/v2/admin/product-variants/%s', $productVariant->getCode()),
server: $header,
content: json_encode([
'channelPricings' => ['WEB' => [
'@id' => sprintf('/api/v2/admin/channel-pricings/%s', $productVariant->getChannelPricingForChannel($channel)->getId()),
'price' => 3000,
'originalPrice' => 4000,
'minimumPrice' => 210,
]],
'translations' => [
'pl_PL' => [
'@id' => sprintf('/api/v2/admin/product-variant-translations/%s', $productVariant->getTranslation('pl_PL')->getId()),
'locale' => 'pl_PL',
'name' => 'Pomarańczowy kubek',
],
'de_DE' => [
'locale' => 'de_DE',
'name' => 'Orange Tasse',
],
]
], JSON_THROW_ON_ERROR),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_variant/put_product_variant_response',
Response::HTTP_OK,
);
}
/** @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()),
'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,
@ -139,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),
);
@ -153,35 +141,34 @@ 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
{
$fixtures = $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);
/** @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()),
'code' => 'MUG_3',
'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,
);
}
@ -189,19 +176,22 @@ final class ProductVariantsTest extends JsonApiTestCase
/** @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']);
$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 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()),
'product' => '/api/v2/admin/products/MUG_SW',
'channelPricings' => ['NON-EXISTING-CHANNEL' => [
'channelCode' => 'NON-EXISTING-CHANNEL',
'price' => 4000,
@ -215,19 +205,22 @@ final class ProductVariantsTest extends JsonApiTestCase
/** @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']);
$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 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()),
'product' => '/api/v2/admin/products/MUG_SW',
'channelPricings' => [
'NON-EXISTING-CHANNEL' => ['price' => 4000]
],
@ -240,7 +233,13 @@ final class ProductVariantsTest extends JsonApiTestCase
/** @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']);
$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(
@ -262,19 +261,22 @@ final class ProductVariantsTest extends JsonApiTestCase
/** @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']);
$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 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()),
'product' => '/api/v2/admin/products/MUG_SW',
'channelPricings' => ['WEB' => [
'channelCode' => 'WEB',
'price' => 4000,
@ -293,10 +295,78 @@ final class ProductVariantsTest extends JsonApiTestCase
$this->assertResponseCode($this->client->getResponse(), Response::HTTP_UNPROCESSABLE_ENTITY);
}
/** @test */
public function it_updates_the_existing_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'];
/** @var ChannelInterface $channel */
$channel = $fixtures['channel_web'];
$this->client->request(
method: 'PUT',
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' => 500,
]],
'translations' => [
'pl_PL' => [
'@id' => sprintf('/api/v2/admin/product-variant-translations/%s', $productVariant->getTranslation('pl_PL')->getId()),
'locale' => 'pl_PL',
'name' => 'Czerwony kubek',
],
'de_DE' => [
'locale' => 'de_DE',
'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),
);
$this->assertResponse(
$this->client->getResponse(),
'admin/product_variant/put_product_variant_response',
Response::HTTP_OK,
);
}
/** @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 */
@ -325,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,
);
}
}

View file

@ -2,28 +2,117 @@ 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'
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'

View file

@ -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."
}

View file

@ -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@
}

View file

@ -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,24 +22,90 @@
"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}",
"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",

View file

@ -1,21 +0,0 @@
{
"@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",
"channelPricings": {
"WEB": {
"@id": "\/api\/v2\/admin\/channel-pricings\/@integer@",
"@type": "ChannelPricing",
"channelCode": "WEB",
"price": 4000,
"originalPrice": 5000,
"minimumPrice": 2000,
"lowestPriceBeforeDiscount": null
}
},
"enabled": false,
"optionValues": [],
"translations": []
}

View file

@ -0,0 +1,34 @@
{
"@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@",
"@type": "ChannelPricing",
"channelCode": "WEB",
"price": 4000,
"originalPrice": null,
"minimumPrice": 0,
"lowestPriceBeforeDiscount": null
}
},
"translations": [],
"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@
}

View file

@ -1,9 +1,12 @@
{
"@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_RED",
"@type": "ProductVariant",
"code": "MUG_RED",
"product": "\/api\/v2\/admin\/products\/MUG_SW",
"optionValues": [
"\/api\/v2\/admin\/product-option-values\/COLOR_RED"
],
"channelPricings": {
"WEB": {
"@id": "\/api\/v2\/admin\/channel-pricings\/@integer@",
@ -15,14 +18,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": "Red mug",
"locale": "en_US"
}
}
},
"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@
}

View file

@ -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_RED"
],
"channelPricings": {
"WEB": {
"@id": "\/api\/v2\/admin\/channel-pricings\/@integer@",
@ -10,25 +15,37 @@
"price": 3000,
"originalPrice": 4000,
"lowestPriceBeforeDiscount": 2000,
"minimumPrice": 210
"minimumPrice": 500
}
},
"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",
"name": "Orange Tasse",
"id": @integer@,
"name": "Rote Tasse",
"locale": "de_DE"
},
"pl_PL": {
"@id": "\/api\/v2\/admin\/product-variant-translations\/@integer@",
"@type": "ProductVariantTranslation",
"name": "Pomarańczowy kubek",
"id": @integer@,
"name": "Czerwony kubek",
"locale": "pl_PL"
}
}
},
"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,
"createdAt": @date@,
"updatedAt": @date@
}

View file

@ -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',