mirror of
https://github.com/Sylius/Sylius.git
synced 2026-07-14 17:10:53 +00:00
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:
commit
9518e82a41
55 changed files with 1696 additions and 312 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ final class ProductOptionValueContext implements Context
|
|||
|
||||
/**
|
||||
* @Transform /^"([^"]+)" option value$/
|
||||
* @Transform :optionValue
|
||||
* @Transform :productOptionValue
|
||||
*/
|
||||
public function getProductOptionValueByCode(string $code): ProductOptionValueInterface
|
||||
{
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ final class ProductVariantContext implements Context
|
|||
/**
|
||||
* @Transform /^"([^"]+)" product variant$/
|
||||
* @Transform /^"([^"]+)" variant$/
|
||||
* @Transform :productVariant
|
||||
* @Transform :variant
|
||||
*/
|
||||
public function getProductVariantByName($name)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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.'
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
@ -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@
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -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@
|
||||
}
|
||||
|
|
@ -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@
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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@
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue