Merge branch '1.13' into 2.0

* 1.13: (82 commits)
  CreateAdminUserCommandTest class - sorting of used classes
  InformAboutGUSCommand class - fix a typo
  Update upgrade file
  [AdminBundle] Change namespace of CreateAdminUser
  [CoreBundle] Move classes from Command to Console/Command
  [UserBundle] Move classes from Command to Console/Command
  [UiBundle] Move classes from Command to Console/Command
  [PromotionBundle] Move classes from Command to Console/Command
  [OrderBundle] Move classes from Command to Console/Command
  [AdminBundle] Move classes from Command to Console/Command
  [ECS] Apply ecs fixes
  [Addressing] Support BC for ZoneMatcher
  [Upgrade] Note ZoneRepository addition and ZoneMatcher constructor changes
  [DX] Update OrderRepository
  [Statistics] Refactor GetStatisticsAction
  Change new orders name to paid orders to not mix up names
  fix unit test
  Apply changes from comments and improve outdated StatisticsModifier
  fix behat tests
  Remove specs for unexisting classes and add existing ones
  ...
This commit is contained in:
Grzegorz Sadowski 2024-01-19 14:50:54 +01:00
commit 1e943288a6
No known key found for this signature in database
GPG key ID: EF87FF4E6E3BD364
229 changed files with 6817 additions and 1867 deletions

View file

@ -80,7 +80,9 @@ class RoboFile extends Tasks
$task->exec('Tests/Application/bin/console doctrine:schema:update --force');
}
$task->exec('vendor/bin/phpspec run --ansi --no-interaction -f dot');
if (file_exists(sprintf('%s/phpspec.yml', $packagePath)) || file_exists(sprintf('%s/phpspec.yml.dist', $packagePath)) || file_exists(sprintf('%s/phpspec.yaml', $packagePath))) {
$task->exec('vendor/bin/phpspec run --ansi --no-interaction -f dot');
}
if (file_exists(sprintf('%s/phpunit.xml', $packagePath)) || file_exists(sprintf('%s/phpunit.xml.dist', $packagePath))) {
$task->exec('vendor/bin/phpunit --colors=always');

View file

@ -262,6 +262,7 @@
},
"autoload": {
"psr-4": {
"Sylius\\Abstraction\\StateMachine\\": "src/Sylius/Abstraction/StateMachine/src",
"Sylius\\Behat\\": "src/Sylius/Behat/",
"Sylius\\Bundle\\": "src/Sylius/Bundle/",
"Sylius\\Component\\": "src/Sylius/Component/"
@ -316,7 +317,8 @@
"spec\\Sylius\\Bundle\\TaxationBundle\\": "src/Sylius/Bundle/TaxationBundle/spec/",
"spec\\Sylius\\Bundle\\TaxonomyBundle\\": "src/Sylius/Bundle/TaxonomyBundle/spec/",
"spec\\Sylius\\Bundle\\UiBundle\\": "src/Sylius/Bundle/UiBundle/spec/",
"spec\\Sylius\\Bundle\\UserBundle\\": "src/Sylius/Bundle/UserBundle/spec/"
"spec\\Sylius\\Bundle\\UserBundle\\": "src/Sylius/Bundle/UserBundle/spec/",
"Tests\\Sylius\\Abstraction\\StateMachine\\": "src/Sylius/Abstraction/StateMachine/tests"
},
"classmap": [
"src/Kernel.php"

View file

@ -6,6 +6,7 @@ return [
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Sylius\Abstraction\StateMachine\SyliusStateMachineAbstractionBundle::class => ['all' => true],
Sylius\Bundle\OrderBundle\SyliusOrderBundle::class => ['all' => true],
Sylius\Bundle\MoneyBundle\SyliusMoneyBundle::class => ['all' => true],
Sylius\Bundle\CurrencyBundle\SyliusCurrencyBundle::class => ['all' => true],

View file

@ -16,14 +16,14 @@ Feature: Resending an order confirmation email for a chosen order
And the customer chose "Free" shipping method with "Cash on Delivery" payment
And I am logged in as an administrator
@ui @email
@ui @email @api
Scenario: Resending a confirmation email for a given order
When I view the summary of the order "#00000666"
And I resend the order confirmation email
Then I should be notified that the order confirmation email has been successfully resent to the customer
And an email with the confirmation of the order "#00000666" should be sent to "lucy@teamlucifer.com"
@ui @email
@ui @email @api
Scenario: Sending a confirmation email after shipping an order in different locale than the default one
Given the order "#00000666" has been placed in "Polish (Poland)" locale
When I view the summary of the order "#00000666"

View file

@ -16,6 +16,7 @@ Feature: Adding a new product variant
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 set its "Taste" option to "Orange"
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
@ -29,6 +30,7 @@ Feature: Adding a new product variant
And I name it "Vodka Wyborowa Premium" in "English (United States)"
And I name it "Wódka Wyborowa Premium" in "Polish (Poland)"
And I set its price to "$100.00" for "United States" channel
And I set its "Taste" option to "Orange"
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
@ -52,6 +54,7 @@ Feature: Adding a new product variant
And I specify its code as "VODKA_WYBOROWA_PREMIUM"
And I set its price to "$100.00" for "United States" channel
And I set its shipping category as "Fragile"
And I set its "Taste" option to "Orange"
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
@ -62,6 +65,7 @@ Feature: Adding a new product variant
And I specify its code as "VODKA_WYBOROWA_EXQUISITE"
And I set its price to "$100.00" for "United States" channel
And I set its original price to "$120.00" for "United States" channel
And I set its "Taste" option to "Orange"
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
@ -74,6 +78,7 @@ Feature: Adding a new product variant
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 variant
And I set its "Taste" option to "Orange"
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
@ -85,6 +90,7 @@ Feature: Adding a new product variant
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 "$0.00" for "United States" channel
And I set its "Taste" option to "Orange"
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
@ -96,6 +102,7 @@ Feature: Adding a new product variant
And I specify its code as "VODKA_WYBOROWA"
And I set its price to "$100.00" for "United States" channel
And I set its minimum price to "$50.00" for "United States" channel
And I set its "Taste" option to "Orange"
And I add it
Then I should be notified that it has been successfully created
And the "VODKA_WYBOROWA" variant of the "Wyborowa Vodka" product should appear in the store

View file

@ -9,7 +9,6 @@ Feature: Editing a product variant
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 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
@ -28,9 +27,7 @@ Feature: Editing a product variant
@api @ui
Scenario: Changing product variant option values
When I want to modify the "Go" product variant
And I set its "Color" option to "Green"
And I change its "Size" option to "L"
And I save my changes
Then I should be notified that it has been successfully edited
And the variant "Go" should have "Color" option as "Green"
And the variant "Go" should have "Size" option as "L"

View file

@ -70,6 +70,31 @@ Feature: Product variant validation
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 @no-ui
Scenario: Adding a new product variant without any of required options configured
Given this product has option "Taste" with values "Orange" and "Melon"
And this product has option "Size" with values "Small" and "Big"
When I want to create a new variant of this product
And I specify its code as "VODKA_WYBOROWA_PREMIUM"
And I do not set its "Taste" and "Size" options
And I set its price to "$100.00" for "United States" channel
And I try to add it
Then I should be notified that required options have not been configured
And the "Wyborowa Vodka" product should have no variants
@api @no-ui
Scenario: Adding a new product variant without one of required options configured
Given this product has option "Taste" with values "Orange" and "Melon"
And this product has option "Size" with values "Small" and "Big"
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 do not set its "Size" option
And I set its price to "$100.00" for "United States" channel
And I try to add it
Then I should be notified that required options have not been configured
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

View file

@ -7,11 +7,11 @@ Feature: Securing access to the administration panel after using the back button
Background:
Given the store operates on a single channel in "United States"
And I am logged in as an administrator
And I am on the administration dashboard
@ui @mink:chromedriver @no-api
Scenario: Securing access to administration dashboard after using the back button after logging out
When I log out
When I am on the administration dashboard
And I log out
And I go back one page in the browser
Then I should not see the administration dashboard
And I should be on the login page

View file

@ -22,11 +22,11 @@ Feature: Statistics
And 2 more new customers have paid 2 orders placed for total of "$5,000.37"
When I view statistics
Then I should see 5 new customers
And I should see 6 new orders
And I should see 6 paid orders
And there should be total sales of "$7,000.58"
And the average order value should be "$1,166.76"
@ui @javascript
@api @ui @javascript
Scenario: Seeing statistics for the previous year
Given it is "first day of January last year" now
And 3 new customers have fulfilled 2 orders placed for total of "$2,000.00"
@ -35,11 +35,11 @@ Feature: Statistics
And 2 more new customers have paid 2 orders placed for total of "$5,000.37"
When I view statistics for "United States" channel and previous year split by month
Then I should see 3 new customers
And I should see 2 new orders
And I should see 2 paid orders
And there should be total sales of "$2,000.00"
And the average order value should be "$1,000.00"
@ui @javascript
@ui @javascript @no-api
Scenario: Seeing statistics for the next year
Given it is "first day of January last year" now
And 3 new customers have fulfilled 2 orders placed for total of "$2,000.00"
@ -49,11 +49,11 @@ Feature: Statistics
When I view statistics for "United States" channel and previous year split by month
And I view statistics for "United States" channel and next year split by month
Then I should see 6 new customers
And I should see 7 new orders
And I should see 7 paid orders
And there should be total sales of "$10,000.74"
And the average order value should be "$1,428.68"
@ui @javascript
@api @ui @javascript
Scenario: Seeing statistics that include only fulfilled orders that were not cancelled
Given 4 new customers have fulfilled 4 orders placed for total of "$5,241.00"
And 2 more new customers have placed 2 orders for total of "$459.00"
@ -61,7 +61,7 @@ Feature: Statistics
And a single customer has placed an order for total of "$1,000.00"
But the customer cancelled this order
When I view statistics for "United States" channel and current year split by month
Then I should see 4 new orders
Then I should see 4 paid orders
And I should see 9 new customers
And there should be total sales of "$5,241.00"
And the average order value should be "$1,310.25"

View file

@ -18,43 +18,43 @@ Feature: Statistics for a specific channel
Given 3 new customers have fulfilled 4 orders placed for total of "$8,566.00" mostly "Onion" product
And 2 more new customers have fulfilled 2 orders placed for total of "$459.00" mostly "Banana" product
When I view statistics
Then I should see 4 new orders
Then I should see 4 paid orders
And I should see 5 new customers
And there should be total sales of "$8,566.00"
And the average order value should be "$2,141.50"
@ui @mink:chromedriver
@ui @mink:chromedriver @api
Scenario: Switching to the channel with only fulfilled orders
Given 4 new customers have fulfilled 4 orders placed for total of "$5,241.00" mostly "Onion" product
And 2 more new customers have fulfilled 2 orders placed for total of "$459.00" mostly "Banana" product
And 2 more new customers have placed 3 orders for total of "$1,259.00" mostly "Banana" product
When I view statistics for "WEB-POLAND" channel and current year split by month
And I choose "WEB-US" channel
Then I should see 2 new orders
Then I should see 2 paid orders
And I should see 8 new customers
And there should be total sales of "$459.00"
And the average order value should be "$229.50"
@ui @mink:chromedriver
@ui @mink:chromedriver @api
Scenario: Switching to the channel with both fulfilled and placed orders
Given 4 new customers have fulfilled 4 orders placed for total of "$5,241.00" mostly "Onion" product
And 2 more new customers have fulfilled 2 orders placed for total of "$459.00" mostly "Banana" product
And 2 more new customers have placed 3 orders for total of "$1,259.00" mostly "Banana" product
When I view statistics for "WEB-US" channel
And I choose "WEB-POLAND" channel
Then I should see 4 new orders
Then I should see 4 paid orders
And I should see 8 new customers
And there should be total sales of "$5,241.00"
And the average order value should be "$1,310.25"
@ui
@no-api @ui
Scenario: Seeing recent orders in a specific channel
Given 3 new customers have placed 4 orders for total of "$8,566.00" mostly "Onion" product
And 2 more new customers have placed 2 orders for total of "$459.00" mostly "Banana" product
When I view statistics for "WEB-POLAND" channel
Then I should see 4 new orders in the list
@ui
@no-api @ui
Scenario: Seeing recent orders in a specific channel
Given 3 new customers have placed 4 orders for total of "$8,566.00" mostly "Onion" product
And 2 more new customers have placed 2 orders for total of "$459.00" mostly "Banana" product

View file

@ -9,7 +9,7 @@ Feature: Changing shop user's password
And there is a user "kibsoon@example.com" identified by "goodGuy"
And I am logged in as an administrator
@ui
@api @ui
Scenario: Changing a password of a shop user
When I change the password of user "kibsoon@example.com" to "veryGoodGuy"
Then I should be notified that it has been successfully edited

View file

@ -9,13 +9,13 @@ Feature: Deleting the customer account
And there is a user "theodore@example.com" identified by "pswd"
And I am logged in as an administrator
@ui
@api @ui
Scenario: Deleting account should not delete customer details
When I delete the account of "theodore@example.com" user
Then the user account should be deleted
But the customer with this email should still exist
@ui
@api @ui
Scenario: A customer with no user cannot be deleted
Given the account of "theodore@example.com" was deleted
Then I should not be able to delete it again

View file

@ -0,0 +1,17 @@
@change_admin_password
Feature: Changing an administrator's password via CLI
In order to login to my administrator account when I forget my password
As a administrator
I want to be able to change my administrator password via CLI
Background:
Given there is an administrator "sylius_change_password@example.com" identified by "sylius"
@cli
Scenario: Changing an administrator's password
When I want to change password
And I specify email as "sylius_change_password@example.com"
And I specify my new password as "newp@ssw0rd"
And I run command
Then I should be informed that password has been changed successfully
And I should be able to log in as "sylius_change_password@example.com" authenticated by "newp@ssw0rd" password

View file

@ -18,7 +18,7 @@ Feature: Seeing validation messages during address addition
Then I should still be on the address addition page
And I should be notified about 6 errors
@ui @javascript @api
@ui @api
Scenario: The province needs to be selected when the chosen country has at least one stated
When I want to add a new address to my address book
And I specify the address as "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States", "Arkansas"

View file

@ -13,7 +13,7 @@ Feature: Province field entry stays after validation errors
And I have an address "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States", "Arkansas" in my address book
And I have an address "Fletcher Ren", "Upper Barkly Street", "3377", "Ararat", "Australia", "Victoria" in my address book
@ui @javascript @api
@ui @api
Scenario: The province name stays after validation error
Given I am editing the address of "Lucifer Morningstar"
When I remove the street
@ -21,7 +21,7 @@ Feature: Province field entry stays after validation errors
Then I should still be on the "Lucifer Morningstar" address edit page
And I should still have "Arkansas" as my specified province
@ui @javascript @api
@ui @api
Scenario: The selected province stays after validation error
Given I am editing the address of "Fletcher Ren"
When I remove the street

View file

@ -80,16 +80,6 @@ parameters:
count: 1
path: src/Sylius/Bundle/AdminBundle/Action/ResendShipmentConfirmationEmailAction.php
-
message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Command\\\\CreateAdminUserCommand\\:\\:askAdminUserData\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/AdminBundle/Command/CreateAdminUserCommand.php
-
message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Command\\\\CreateAdminUserCommand\\:\\:showSummary\\(\\) has parameter \\$adminUserData with no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/AdminBundle/Command/CreateAdminUserCommand.php
-
message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Controller\\\\CustomerStatisticsController\\:\\:__construct\\(\\) has parameter \\$customerRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#"
count: 1
@ -190,36 +180,6 @@ parameters:
count: 1
path: src/Sylius/Bundle/AdminBundle/Menu/PromotionUpdateMenuBuilder.php
-
message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\MessageHandler\\\\CreateAdminUserHandler\\:\\:__construct\\(\\) has parameter \\$adminUserFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#"
count: 1
path: src/Sylius/Bundle/AdminBundle/MessageHandler/CreateAdminUserHandler.php
-
message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\MessageHandler\\\\CreateAdminUserHandler\\:\\:__construct\\(\\) has parameter \\$adminUserRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#"
count: 1
path: src/Sylius/Bundle/AdminBundle/MessageHandler/CreateAdminUserHandler.php
-
message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\MessageHandler\\\\CreateAdminUserHandler\\:\\:__construct\\(\\) has parameter \\$validationGroups with no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/AdminBundle/MessageHandler/CreateAdminUserHandler.php
-
message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\MessageHandler\\\\CreateAdminUserHandler\\:\\:getViolationMessages\\(\\) return type has no value type specified in iterable type iterable\\.$#"
count: 1
path: src/Sylius/Bundle/AdminBundle/MessageHandler/CreateAdminUserHandler.php
-
message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Provider\\\\StatisticsDataProvider\\:\\:getRawData\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/AdminBundle/Provider/StatisticsDataProvider.php
-
message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Provider\\\\StatisticsDataProviderInterface\\:\\:getRawData\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/AdminBundle/Provider/StatisticsDataProviderInterface.php
-
message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Twig\\\\ChannelNameExtension\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#"
count: 1
@ -2295,21 +2255,6 @@ parameters:
count: 1
path: src/Sylius/Bundle/CoreBundle/Collector/SyliusCollector.php
-
message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Command\\\\AbstractInstallCommand\\:\\:renderTable\\(\\) has parameter \\$headers with no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Command/AbstractInstallCommand.php
-
message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Command\\\\AbstractInstallCommand\\:\\:renderTable\\(\\) has parameter \\$rows with no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Command/AbstractInstallCommand.php
-
message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Command\\\\AbstractInstallCommand\\:\\:runCommands\\(\\) has parameter \\$commands with no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Command/AbstractInstallCommand.php
-
message: "#^PHPDoc tag @var for variable \\$command has no value type specified in iterable type array\\.$#"
count: 1
@ -3990,11 +3935,6 @@ parameters:
count: 1
path: src/Sylius/Bundle/LocaleBundle/SyliusLocaleBundle.php
-
message: "#^Method Sylius\\\\Bundle\\\\OrderBundle\\\\Command\\\\RemoveExpiredCartsCommand\\:\\:__construct\\(\\) has parameter \\$expirationTime with no type specified\\.$#"
count: 1
path: src/Sylius/Bundle/OrderBundle/Command/RemoveExpiredCartsCommand.php
-
message: "#^Method Sylius\\\\Bundle\\\\OrderBundle\\\\Context\\\\SessionBasedCartContext\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Order\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#"
count: 1
@ -4330,11 +4270,6 @@ parameters:
count: 1
path: src/Sylius/Bundle/ProductBundle/Validator/UniqueSimpleProductCodeValidator.php
-
message: "#^Method Sylius\\\\Bundle\\\\PromotionBundle\\\\Command\\\\GenerateCouponsCommand\\:\\:__construct\\(\\) has parameter \\$promotionRepository with generic interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\PromotionRepositoryInterface but does not specify its types\\: T$#"
count: 1
path: src/Sylius/Bundle/PromotionBundle/Command/GenerateCouponsCommand.php
-
message: "#^Method Sylius\\\\Bundle\\\\PromotionBundle\\\\DiscountApplicationCriteria\\\\DiscountApplicationCriteriaInterface\\:\\:isApplicable\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#"
count: 1
@ -4871,24 +4806,14 @@ parameters:
path: src/Sylius/Bundle/UiBundle/Twig/TemplateEventExtension.php
-
message: "#^Method Sylius\\\\Bundle\\\\UserBundle\\\\Command\\\\AbstractRoleCommand\\:\\:executeRoleCommand\\(\\) has parameter \\$securityRoles with no value type specified in iterable type array\\.$#"
message: "#^Method Sylius\\\\Bundle\\\\UserBundle\\\\Console\\\\Command\\\\AbstractRoleCommand\\:\\:getUserRepository\\(\\) return type with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface does not specify its types\\: T$#"
count: 1
path: src/Sylius/Bundle/UserBundle/Command/AbstractRoleCommand.php
-
message: "#^Method Sylius\\\\Bundle\\\\UserBundle\\\\Command\\\\AbstractRoleCommand\\:\\:getAvailableUserTypes\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Sylius/Bundle/UserBundle/Command/AbstractRoleCommand.php
-
message: "#^Method Sylius\\\\Bundle\\\\UserBundle\\\\Command\\\\AbstractRoleCommand\\:\\:getUserRepository\\(\\) return type with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface does not specify its types\\: T$#"
count: 1
path: src/Sylius/Bundle/UserBundle/Command/AbstractRoleCommand.php
path: src/Sylius/Bundle/UserBundle/Console/Command/AbstractRoleCommand.php
-
message: "#^Unable to resolve the template type T in call to method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:getRepository\\(\\)$#"
count: 1
path: src/Sylius/Bundle/UserBundle/Command/AbstractRoleCommand.php
path: src/Sylius/Bundle/UserBundle/Console/Command/AbstractRoleCommand.php
-
message: "#^Method Sylius\\\\Bundle\\\\UserBundle\\\\Command\\\\DemoteUserCommand\\:\\:executeRoleCommand\\(\\) has parameter \\$securityRoles with no value type specified in iterable type array\\.$#"
@ -6719,3 +6644,88 @@ parameters:
message: "#^Interface Sylius\\\\Bundle\\\\UserBundle\\\\Provider\\\\UserProviderInterface extends generic interface Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserProviderInterface but does not specify its types\\: TUser$#"
count: 1
path: src/Sylius/Bundle/UserBundle/Provider/UserProviderInterface.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Command/CancelUnpaidOrdersCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Command/CheckRequirementsCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Command/InformAboutGUSCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Command/InstallAssetsCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Command/InstallCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Command/InstallDatabaseCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Command/InstallSampleDataCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Command/JwtConfigurationCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Command/SetupCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Command/ShowAvailablePluginsCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Command/Model/PluginInfo.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/OrderBundle/Command/RemoveExpiredCartsCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/PromotionBundle/Command/GenerateCouponsCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/UiBundle/Command/DebugTemplateEventCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/UserBundle/Command/AbstractRoleCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/UserBundle/Command/DemoteUserCommand.php
-
message: "#^If condition is always false.$#"
count: 1
path: src/Sylius/Bundle/UserBundle/Command/PromoteUserCommand.php

View file

@ -0,0 +1,3 @@
/vendor
/composer.lock
/.phpunit.result.cache

View file

@ -0,0 +1,43 @@
{
"name": "sylius/state-machine-abstraction",
"description": "Abstraction layer for State Machine used in Sylius bundles",
"type": "symfony-bundle",
"license": "MIT",
"authors": [
{
"name": "Jacob Tobiasz",
"email": "jakub.tobiasz@icloud.com"
}
],
"autoload": {
"psr-4": {
"Sylius\\Abstraction\\StateMachine\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\Sylius\\Abstraction\\StateMachine\\": "tests/"
}
},
"require": {
"php": "^8.0",
"symfony/dependency-injection": "^5.4.21 || ^6.0",
"symfony/http-kernel": "^5.4.21 || ^6.0",
"symfony/workflow": "^5.4.21 || ^6.0",
"winzou/state-machine": "^0.4",
"winzou/state-machine-bundle": "^0.6"
},
"require-dev": {
"matthiasnoback/symfony-config-test": "^4.2",
"phpunit/phpunit": "^9.5",
"webmozart/assert": "^1.9"
},
"extra": {
"branch-alias": {
"dev-master": "1.12-dev"
},
"symfony": {
"require": "^5.4.21"
}
}
}

View file

@ -0,0 +1,41 @@
<?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.
-->
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults public="true" />
<service id="sylius_abstraction.state_machine.composite" class="Sylius\Abstraction\StateMachine\CompositeStateMachine">
<argument type="tagged_iterator" tag="sylius.state_machine" index-by="key" />
<argument>%sylius_abstraction.state_machine.default_adapter%</argument>
<argument>%sylius_abstraction.state_machine.graphs_to_adapters_mapping%</argument>
</service>
<service id="Sylius\Abstraction\StateMachine\StateMachineInterface $compositeStateMachine" alias="sylius_abstraction.state_machine.composite" />
<service id="sylius_abstraction.state_machine" alias="sylius_abstraction.state_machine.composite" />
<service id="Sylius\Abstraction\StateMachine\StateMachineInterface" alias="sylius_abstraction.state_machine" />
<service id="sylius_abstraction.state_machine.adapter.symfony_workflow" class="Sylius\Abstraction\StateMachine\SymfonyWorkflowAdapter">
<argument type="service" id="workflow.registry" />
<tag name="sylius.state_machine" key="symfony_workflow" />
</service>
<service id="Sylius\Abstraction\StateMachine\StateMachineInterface $symfonyWorkflow" alias="sylius_abstraction.state_machine.adapter.symfony_workflow" />
<service id="sylius_abstraction.state_machine.adapter.winzou_state_machine" class="Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter">
<argument type="service" id="sm.factory" />
<tag name="sylius.state_machine" key="winzou_state_machine" />
</service>
<service id="Sylius\Abstraction\StateMachine\StateMachineInterface $winzouStateMachine" alias="sylius_abstraction.state_machine.adapter.winzou_state_machine" />
</services>
</container>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- http://phpunit.de/manual/4.1/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.5/phpunit.xsd"
colors="true"
convertDeprecationsToExceptions="true"
>
<testsuites>
<testsuite name="all">
<directory>tests</directory>
</testsuite>
</testsuites>
<php>
<ini name="error_reporting" value="-1" />
</php>
</phpunit>

View file

@ -11,7 +11,7 @@
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\StateMachine;
namespace Sylius\Abstraction\StateMachine;
use Traversable;
use Webmozart\Assert\Assert;

View file

@ -0,0 +1,46 @@
<?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\Abstraction\StateMachine\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
final class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('sylius_abstraction');
/** @var ArrayNodeDefinition $rootNode */
$rootNode = $treeBuilder->getRootNode();
$rootNode
->addDefaultsIfNotSet()
->children()
->arrayNode('state_machine')
->addDefaultsIfNotSet()
->children()
->scalarNode('default_adapter')->defaultValue('winzou_state_machine')->end()
->arrayNode('graphs_to_adapters_mapping')
->useAttributeAsKey('graph_name')
->scalarPrototype()->end()
->end()
->end()
->end()
->end()
;
return $treeBuilder;
}
}

View file

@ -0,0 +1,33 @@
<?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\Abstraction\StateMachine\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
final class SyliusStateMachineAbstractionExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
$config = $this->processConfiguration($this->getConfiguration([], $container), $configs);
$loader = new XmlFileLoader($container, new FileLocator(dirname(__DIR__, 2) . '/config/'));
$loader->load('services.xml');
$container->setParameter('sylius_abstraction.state_machine.default_adapter', $config['state_machine']['default_adapter']);
$container->setParameter('sylius_abstraction.state_machine.graphs_to_adapters_mapping', $config['state_machine']['graphs_to_adapters_mapping']);
}
}

View file

@ -11,7 +11,7 @@
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\StateMachine\Exception;
namespace Sylius\Abstraction\StateMachine\Exception;
class StateMachineExecutionException extends \Exception
{

View file

@ -11,9 +11,9 @@
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\StateMachine;
namespace Sylius\Abstraction\StateMachine;
use Sylius\Bundle\CoreBundle\StateMachine\Exception\StateMachineExecutionException;
use Sylius\Abstraction\StateMachine\Exception\StateMachineExecutionException;
interface StateMachineInterface
{

View file

@ -0,0 +1,24 @@
<?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\Abstraction\StateMachine;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class SyliusStateMachineAbstractionBundle extends Bundle
{
public function getPath(): string
{
return dirname(__DIR__);
}
}

View file

@ -11,9 +11,9 @@
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\StateMachine;
namespace Sylius\Abstraction\StateMachine;
use Sylius\Bundle\CoreBundle\StateMachine\Exception\StateMachineExecutionException;
use Sylius\Abstraction\StateMachine\Exception\StateMachineExecutionException;
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\Transition as SymfonyWorkflowTransition;

View file

@ -11,7 +11,7 @@
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\StateMachine;
namespace Sylius\Abstraction\StateMachine;
final class Transition implements TransitionInterface
{

View file

@ -11,7 +11,7 @@
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\StateMachine;
namespace Sylius\Abstraction\StateMachine;
interface TransitionInterface
{

View file

@ -11,11 +11,11 @@
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\StateMachine;
namespace Sylius\Abstraction\StateMachine;
use SM\Factory\FactoryInterface;
use SM\SMException;
use Sylius\Bundle\CoreBundle\StateMachine\Exception\StateMachineExecutionException;
use Sylius\Abstraction\StateMachine\Exception\StateMachineExecutionException;
final class WinzouStateMachineAdapter implements StateMachineInterface
{

View file

@ -0,0 +1,77 @@
<?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 Tests\Sylius\Abstraction\StateMachine\Functional\DependencyInjection;
use Matthias\SymfonyConfigTest\PhpUnit\ConfigurationTestCaseTrait;
use PHPUnit\Framework\TestCase;
use Sylius\Abstraction\StateMachine\DependencyInjection\Configuration;
use Symfony\Component\Config\Definition\ConfigurationInterface;
final class ConfigurationTest extends TestCase
{
use ConfigurationTestCaseTrait;
/** @test */
public function it_allows_to_configure_a_default_state_machine_adapter(): void
{
$this->assertProcessedConfigurationEquals(
[
[
'state_machine' => [
'default_adapter' => 'symfony_workflow',
],
],
],
[
'state_machine' => [
'default_adapter' => 'symfony_workflow',
'graphs_to_adapters_mapping' => [],
],
],
'state_machine',
);
}
/** @test */
public function it_allows_to_configure_the_state_machines_adapters_mapping(): void
{
$this->assertProcessedConfigurationEquals(
[
[
'state_machine' => [
'graphs_to_adapters_mapping' => [
'order' => 'symfony_workflow',
'payment' => 'winzou_state_machine',
],
],
],
],
[
'state_machine' => [
'default_adapter' => 'winzou_state_machine',
'graphs_to_adapters_mapping' => [
'order' => 'symfony_workflow',
'payment' => 'winzou_state_machine',
],
],
],
'state_machine',
);
}
protected function getConfiguration(): ConfigurationInterface
{
return new Configuration();
}
}

View file

@ -0,0 +1,89 @@
<?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 Tests\Sylius\Abstraction\StateMachine\Unit;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Sylius\Abstraction\StateMachine\CompositeStateMachine;
use Sylius\Abstraction\StateMachine\StateMachineInterface;
use Webmozart\Assert\InvalidArgumentException;
final class CompositeStateMachineTest extends TestCase
{
/** @var StateMachineInterface&MockObject */
private StateMachineInterface $someStateMachineAdapter;
/** @var StateMachineInterface&MockObject */
private StateMachineInterface $anotherStateMachineAdapter;
protected function setUp(): void
{
parent::setUp();
$this->someStateMachineAdapter = $this->createMock(StateMachineInterface::class);
$this->anotherStateMachineAdapter = $this->createMock(StateMachineInterface::class);
}
public function testItThrowsAnExceptionIfNoStateMachineAdapterIsProvided(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('At least one state machine adapter should be provided.');
$this->createTestSubject(stateMachineAdapters: []);
}
public function testItThrowsAnExceptionIfStateMachineAdapterDoesNotImplementTheInterface(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('All state machine adapters should implement the "Sylius\Abstraction\StateMachine\StateMachineInterface" interface.');
$this->createTestSubject(stateMachineAdapters: [$this->createMock(\stdClass::class)]);
}
public function testItUsesMappedAdapterToUseWhetherTransitionCanBeApplied(): void
{
$this->someStateMachineAdapter->expects($this->never())->method('can');
$this->anotherStateMachineAdapter->expects($this->once())->method('can')->willReturn(true);
$stateMachine = $this->createTestSubject();
$this->assertTrue($stateMachine->can(new \stdClass(), 'another_graph', 'some_transition'));
}
public function testItAppliesTransitionUsingMappedAdapter(): void
{
$this->someStateMachineAdapter->expects($this->once())->method('apply');
$this->anotherStateMachineAdapter->expects($this->never())->method('apply');
$stateMachine = $this->createTestSubject();
$stateMachine->apply(new \stdClass(), 'some_graph', 'some_transition');
}
public function testItReturnsEnabledTransitionsUsingMappedAdapter(): void
{
$this->someStateMachineAdapter->expects($this->never())->method('getEnabledTransitions');
$this->anotherStateMachineAdapter->expects($this->once())->method('getEnabledTransitions')->willReturn(['some_transition']);
$stateMachine = $this->createTestSubject();
$this->assertSame(['some_transition'], $stateMachine->getEnabledTransitions(new \stdClass(), 'another_graph'));
}
private function createTestSubject(mixed ...$arguments): StateMachineInterface
{
return new CompositeStateMachine(...array_replace([
'stateMachineAdapters' => ['some_adapter' => $this->someStateMachineAdapter, 'another_adapter' => $this->anotherStateMachineAdapter],
'defaultAdapter' => 'some_adapter',
'graphsToAdaptersMapping' => ['some_graph' => 'some_adapter', 'another_graph' => 'another_adapter'],
], $arguments));
}
}

View file

@ -0,0 +1,162 @@
<?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 Tests\Sylius\Abstraction\StateMachine\Unit;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Sylius\Abstraction\StateMachine\Exception\StateMachineExecutionException;
use Sylius\Abstraction\StateMachine\StateMachineInterface;
use Sylius\Abstraction\StateMachine\SymfonyWorkflowAdapter;
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\Transition as SymfonyWorkflowTransition;
use Symfony\Component\Workflow\Workflow;
final class SymfonyWorkflowAdapterTest extends TestCase
{
/** @var Registry&MockObject */
private Registry $symfonyWorkflowRegistry;
protected function setUp(): void
{
parent::setUp();
$this->symfonyWorkflowRegistry = $this->createMock(Registry::class);
}
public function testItReturnWhetherTransitionCanBeApplied(): void
{
$subject = new \stdClass();
$graphName = 'graph_name';
$transition = 'transition';
$workflow = $this->createMock(Workflow::class);
$workflow
->expects($this->once())
->method('can')
->with($subject, $transition)
->willReturn(true)
;
$this->symfonyWorkflowRegistry
->expects($this->once())
->method('get')
->with($subject, $graphName)
->willReturn($workflow)
;
$this->createTestSubject()->can($subject, $graphName, $transition);
}
public function testItAppliesTransition(): void
{
$subject = new \stdClass();
$graphName = 'graph_name';
$transition = 'transition';
/** @var array<string, mixed> $context */
$context = ['context'];
$workflow = $this->createMock(Workflow::class);
$workflow
->expects($this->once())
->method('apply')
->with($subject, $transition, $context)
;
$this->symfonyWorkflowRegistry
->expects($this->once())
->method('get')
->with($subject, $graphName)
->willReturn($workflow)
;
$this->createTestSubject()->apply($subject, $graphName, $transition, $context);
}
public function testItReturnsEnabledTransitions(): void
{
$subject = new \stdClass();
$graphName = 'graph_name';
$workflow = $this->createMock(Workflow::class);
$workflow
->expects($this->once())
->method('getEnabledTransitions')
->with($subject)
->willReturn([$this->createSampleTransition()])
;
$this->symfonyWorkflowRegistry
->expects($this->once())
->method('get')
->with($subject, $graphName)
->willReturn($workflow)
;
$enabledTransition = $this->createTestSubject()->getEnabledTransitions($subject, $graphName);
$this->assertCount(1, $enabledTransition);
$this->assertSame('transition', $enabledTransition[0]->getName());
$this->assertSame(['from'], $enabledTransition[0]->getFroms());
$this->assertSame(['to'], $enabledTransition[0]->getTos());
}
public function testItConvertsWorkflowExceptionsToCustomOnesOnCan(): void
{
$this->expectException(StateMachineExecutionException::class);
$subject = new \stdClass();
$graphName = 'graph_name';
$transition = 'transition';
$this->symfonyWorkflowRegistry->method('get')->willThrowException(new InvalidArgumentException());
$this->createTestSubject()->can($subject, $graphName, $transition);
}
public function testItConvertsWorkflowExceptionsToCustomOnApply(): void
{
$this->expectException(StateMachineExecutionException::class);
$subject = new \stdClass();
$graphName = 'graph_name';
$transition = 'transition';
$this->symfonyWorkflowRegistry->method('get')->willThrowException(new InvalidArgumentException());
$this->createTestSubject()->apply($subject, $graphName, $transition);
}
public function testItConvertsWorkflowExceptionsToCustomOnGetEnabledTransitions(): void
{
$this->expectException(StateMachineExecutionException::class);
$subject = new \stdClass();
$graphName = 'graph_name';
$this->symfonyWorkflowRegistry->method('get')->willThrowException(new InvalidArgumentException());
$this->createTestSubject()->getEnabledTransitions($subject, $graphName);
}
private function createTestSubject(): StateMachineInterface
{
return new SymfonyWorkflowAdapter($this->symfonyWorkflowRegistry);
}
private function createSampleTransition(): SymfonyWorkflowTransition
{
return new SymfonyWorkflowTransition('transition', ['from'], ['to']);
}
}

View file

@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Tests\Sylius\Abstraction\StateMachine\Unit;
use PHPUnit\Framework\TestCase;
use Sylius\Abstraction\StateMachine\Transition;
final class TransitionTest extends TestCase
{
public function testItReturnsItsName(): void
{
$this->assertSame('name', $this->createTestSubject()->getName());
}
public function testItReturnsItsFroms(): void
{
$this->assertSame(['from'], $this->createTestSubject()->getFroms());
}
public function testItReturnsItsTos(): void
{
$this->assertSame(['to'], $this->createTestSubject()->getTos());
}
private function createTestSubject(): Transition
{
return new Transition('name', ['from'], ['to']);
}
}

View file

@ -0,0 +1,125 @@
<?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 Tests\Sylius\Abstraction\StateMachine\Unit;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use SM\Factory\FactoryInterface;
use SM\SMException;
use SM\StateMachine\StateMachineInterface as WinzouStateMachineInterface;
use Sylius\Abstraction\StateMachine\Exception\StateMachineExecutionException;
use Sylius\Abstraction\StateMachine\StateMachineInterface;
use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter;
final class WinzouStateMachineAdapterTest extends TestCase
{
/** @var FactoryInterface&MockObject */
private FactoryInterface $winzouStateMachineFactory;
/** @var WinzouStateMachineInterface&MockObject */
private WinzouStateMachineInterface $winzouStateMachine;
protected function setUp(): void
{
parent::setUp();
$this->winzouStateMachine = $this->createMock(WinzouStateMachineInterface::class);
$this->winzouStateMachineFactory = $this->createMock(FactoryInterface::class);
$this->winzouStateMachineFactory
->method('get')
->willReturn($this->winzouStateMachine)
;
}
public function testItReturnWhetherTransitionCanBeApplied(): void
{
$this->winzouStateMachine->method('can')->willReturn(true);
$subject = new \stdClass();
$graphName = 'graph_name';
$transition = 'transition';
$this->assertTrue($this->createTestSubject()->can($subject, $graphName, $transition));
}
public function testItAppliesTransition(): void
{
$this->winzouStateMachine->expects($this->once())->method('apply');
$subject = new \stdClass();
$graphName = 'graph_name';
$transition = 'transition';
$this->createTestSubject()->apply($subject, $graphName, $transition);
}
public function testItReturnsEnabledTransitions(): void
{
$this->winzouStateMachine->method('getPossibleTransitions')->willReturn(['transition']);
$subject = new \stdClass();
$graphName = 'graph_name';
$transitions = $this->createTestSubject()->getEnabledTransitions($subject, $graphName);
$this->assertCount(1, $transitions);
$this->assertSame('transition', $transitions[0]->getName());
$this->assertNull($transitions[0]->getFroms());
$this->assertNull($transitions[0]->getTos());
}
public function testItConvertsWorkflowExceptionsToCustomOnesOnCan(): void
{
$this->expectException(StateMachineExecutionException::class);
$subject = new \stdClass();
$graphName = 'graph_name';
$transition = 'transition';
$this->winzouStateMachineFactory->method('get')->willThrowException(new SMException());
$this->createTestSubject()->can($subject, $graphName, $transition);
}
public function testItConvertsWorkflowExceptionsToCustomOnApply(): void
{
$this->expectException(StateMachineExecutionException::class);
$subject = new \stdClass();
$graphName = 'graph_name';
$transition = 'transition';
$this->winzouStateMachineFactory->method('get')->willThrowException(new SMException());
$this->createTestSubject()->apply($subject, $graphName, $transition);
}
public function testItConvertsWorkflowExceptionsToCustomOnGetEnabledTransitions(): void
{
$this->expectException(StateMachineExecutionException::class);
$subject = new \stdClass();
$graphName = 'graph_name';
$this->winzouStateMachineFactory->method('get')->willThrowException(new SMException());
$this->createTestSubject()->getEnabledTransitions($subject, $graphName);
}
private function createTestSubject(): StateMachineInterface
{
return new WinzouStateMachineAdapter($this->winzouStateMachineFactory);
}
}

View file

@ -0,0 +1,157 @@
<?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\Client\ResponseCheckerInterface;
use Sylius\Calendar\Provider\DateTimeProviderInterface;
use Sylius\Component\Core\Model\ChannelInterface;
use Webmozart\Assert\Assert;
final class DashboardContext implements Context
{
public function __construct(
private ApiClientInterface $client,
private ResponseCheckerInterface $responseChecker,
private DateTimeProviderInterface $dateTimeProvider,
) {
}
/**
* @When I view statistics
*/
public function iBrowseStatistics(): void
{
$this->client->index('statistics');
}
/**
* @When I view statistics for :channel channel and current year split by month
* @When I choose :channel channel
* @When I view statistics for :channel channel
*/
public function iViewStatisticsForChannelAndYear(ChannelInterface $channel): void
{
$this->client->index(
'statistics',
[
'channelCode' => $channel->getCode(),
'startDate' => $this->dateTimeProvider->now()->format('Y-01-01\T00:00:00'),
'dateInterval' => 'P1M',
'endDate' => $this->dateTimeProvider->now()->format('Y-12-31\T23:59:59'),
],
);
}
/**
* @When I view statistics for :channel channel and previous year split by month
*/
public function iViewStatisticsForChannelAndPreviousYear(ChannelInterface $channel): void
{
$currentYear = (int) $this->dateTimeProvider->now()->format('Y');
$this->client->index(
'statistics',
[
'channelCode' => $channel->getCode(),
'startDate' => ($currentYear - 1) . '-01-01T00:00:00',
'dateInterval' => 'P1M',
'endDate' => ($currentYear - 1) . '-12-31T23:59:59',
],
);
}
/**
* @When I view statistics for :channel channel and next year split by month
*/
public function iViewStatisticsForChannelAndNextYear(ChannelInterface $channel): void
{
$currentYear = (int) $this->dateTimeProvider->now()->format('Y');
$this->client->index(
'statistics',
[
'channelCode' => $channel->getCode(),
'startDate' => ($currentYear + 1) . '-01-01T00:00:00',
'dateInterval' => 'P1M',
'endDate' => ($currentYear + 1) . '-12-31T23:59:59',
],
);
}
/**
* @Then I should see :count paid orders
*/
public function iShouldSeeNewOrders(int $count): void
{
Assert::true(
$this->responseChecker->hasValuesInSubresourceObject(
$this->client->getLastResponse(),
'businessActivitySummary',
['paidOrdersCount' => $count],
),
);
}
/**
* @Then I should see :number new customers( in the list)
*/
public function iShouldSeeNewCustomers(int $count): void
{
Assert::true(
$this->responseChecker->hasValuesInSubresourceObject(
$this->client->getLastResponse(),
'businessActivitySummary',
['newCustomersCount' => $count],
),
sprintf(
'There should be %s new customers, but got %s.',
$count,
json_encode(
$this->responseChecker->getValue($this->client->getLastResponse(), 'businessActivitySummary'),
),
),
);
}
/**
* @Then /^there should be total sales of ("[^"]+")$/
*/
public function thereShouldBeTotalSalesOf(int $totalSales): void
{
Assert::true(
$this->responseChecker->hasValuesInSubresourceObject(
$this->client->getLastResponse(),
'businessActivitySummary',
['totalSales' => $totalSales],
),
);
}
/**
* @Then /^the average order value should be ("[^"]+")$/
*/
public function myAverageOrderValueShouldBe(int $averageTotalValue): void
{
Assert::true(
$this->responseChecker->hasValuesInSubresourceObject(
$this->client->getLastResponse(),
'businessActivitySummary',
['averageOrderValue' => $averageTotalValue],
),
sprintf('Average order value should be %s, but it does not.', $averageTotalValue),
);
}
}

View file

@ -18,8 +18,10 @@ use Behat\Behat\Context\Context;
use Sylius\Behat\Client\ApiClientInterface;
use Sylius\Behat\Client\ResponseCheckerInterface;
use Sylius\Behat\Context\Api\Resources;
use Sylius\Behat\Service\SharedStorageInterface;
use Sylius\Component\Addressing\Model\CountryInterface;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\Customer\Model\CustomerGroupInterface;
use Webmozart\Assert\Assert;
@ -31,6 +33,7 @@ final class ManagingCustomersContext implements Context
private ApiClientInterface $client,
private ResponseCheckerInterface $responseChecker,
private IriConverterInterface $iriConverter,
private SharedStorageInterface $sharedStorage,
) {
}
@ -210,6 +213,25 @@ final class ManagingCustomersContext implements Context
]);
}
/**
* @When I change the password of user :customer to :newPassword
*/
public function iChangeThePasswordOfUserTo(CustomerInterface $customer, string $newPassword): void
{
$this->iWantToEditThisCustomer($customer);
$this->iSpecifyItsPasswordAs($newPassword);
$this->client->update();
}
/**
* @When I delete the account of :shopUser user
*/
public function iDeleteAccount(ShopUserInterface $shopUser): void
{
$this->sharedStorage->set('customer', $shopUser->getCustomer());
$this->client->delete(sprintf('customer/%s', $shopUser->getCustomer()->getId()), 'user');
}
/**
* @When I do not specify any information
* @When I do not choose create account option
@ -561,6 +583,20 @@ final class ManagingCustomersContext implements Context
);
}
/**
* @Then the customer with this email should still exist
*/
public function customerShouldStillExist(): void
{
/** @var CustomerInterface $customer */
$customer = $this->sharedStorage->get('customer');
$this->client->show(Resources::CUSTOMERS, (string) $customer->getId());
Assert::same($this->client->getLastResponse()->getStatusCode(), 200);
Assert::same($this->responseChecker->getValue($this->client->getLastResponse(), 'email'), $customer->getEmail());
}
/**
* @Then I should not see create account option
* @Then I should still be on the customer creation page
@ -572,4 +608,28 @@ final class ManagingCustomersContext implements Context
public function intentionallyLeftBlank(): void
{
}
/**
* @Then the user account should be deleted
*/
public function accountShouldBeDeleted(): void
{
/** @var CustomerInterface $customer */
$customer = $this->sharedStorage->get('customer');
$response = $this->client->show(Resources::CUSTOMERS, (string) $customer->getId());
Assert::null($this->responseChecker->getValue($response, 'user'));
}
/**
* @Then I should not be able to delete it again
*/
public function iShouldNotBeAbleToDeleteCustomerAgain(): void
{
$customer = $this->sharedStorage->get('customer');
$this->client->delete(sprintf('customer/%s', $customer->getId()), 'user');
Assert::same($this->client->getLastResponse()->getStatusCode(), 404);
}
}

View file

@ -36,6 +36,7 @@ use Sylius\Component\Currency\Model\CurrencyInterface;
use Sylius\Component\Order\OrderTransitions;
use Sylius\Component\Payment\PaymentTransitions;
use Sylius\Component\Shipping\ShipmentTransitions;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Intl\Countries;
use Webmozart\Assert\Assert;
@ -45,7 +46,6 @@ final class ManagingOrdersContext implements Context
public function __construct(
private ApiClientInterface $client,
private ResponseCheckerInterface $responseChecker,
private RequestFactoryInterface $requestFactory,
private IriConverterInterface $iriConverter,
private SecurityServiceInterface $adminSecurityService,
private SharedStorageInterface $sharedStorage,
@ -139,6 +139,19 @@ final class ManagingOrdersContext implements Context
$this->client->addFilter('checkoutCompletedAt[before]', $dateTime);
}
/**
* @When I resend the order confirmation email
*/
public function iResendTheOrderConfirmationEmail(): void
{
$this->client->customItemAction(
Resources::ORDERS,
$this->sharedStorage->get('order')->getTokenValue(),
HttpRequest::METHOD_POST,
'resend-confirmation-email',
);
}
/**
* @When I filter by product :productName
* @When I filter by products :firstProduct and :secondProduct
@ -312,6 +325,14 @@ final class ManagingOrdersContext implements Context
Assert::same($this->responseChecker->countCollectionItems($this->client->getLastResponse()), $number);
}
/**
* @Then I should be notified that the order confirmation email has been successfully resent to the customer
*/
public function iShouldBeNotifiedThatTheOrderConfirmationEmailHasBeenSuccessfullyResentToTheCustomer(): void
{
$this->responseChecker->isCreationSuccessful($this->client->getLastResponse());
}
/**
* @Then I should be notified that it has been successfully updated
*/

View file

@ -98,6 +98,8 @@ final class ManagingProductVariantsContext implements Context
/**
* @When I do not set its price
* @When I do not specify its code
* @When I do not set its :optionName option
* @When I do not set its :firstOptionName and :secondOptionName options
*/
public function iDoNotSetValue(): void
{
@ -723,6 +725,17 @@ final class ManagingProductVariantsContext implements Context
);
}
/**
* @Then I should be notified that required options have not been configured
*/
public function iShouldBeNotifiedThatRequiredOptionsHaveNotBeenConfigured(): void
{
Assert::contains(
$this->responseChecker->getError($this->client->getLastResponse()),
'The product variant must have configured values for all options chosen on the product.',
);
}
/**
* @Then I should be notified that on hand quantity must be greater than the number of on hold units
*/

View file

@ -0,0 +1,98 @@
<?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\Cli;
use Behat\Behat\Context\Context;
use Sylius\Component\Core\Model\AdminUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Sylius\Component\User\Security\UserPasswordHasherInterface;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\HttpKernel\KernelInterface;
use Webmozart\Assert\Assert;
final class ChangeAdminPasswordContext implements Context
{
private const ADMIN_USER_CHANGE_PASSWORD = 'sylius:admin-user:change-password';
private Application $application;
private ?CommandTester $commandTester = null;
private $input = [];
/** @param UserRepositoryInterface<AdminUserInterface> $adminUserRepository */
public function __construct(
KernelInterface $kernel,
private UserRepositoryInterface $adminUserRepository,
private UserPasswordHasherInterface $userPasswordHasher,
) {
$this->application = new Application($kernel);
}
/**
* @When I want to change password
*/
public function iWantToChangePassword(): void
{
$command = $this->application->find(self::ADMIN_USER_CHANGE_PASSWORD);
$this->commandTester = new CommandTester($command);
}
/**
* @When I specify email as :email
*/
public function iSpecifyEmailAs(string $email = ''): void
{
$this->input['email'] = $email;
}
/**
* @When I specify my new password as :password
*/
public function iSpecifyMyNewPassword(string $password = ''): void
{
$this->input['password'] = $password;
}
/**
* @When I run command
*/
public function iRunCommand(): void
{
$this->commandTester->setInputs($this->input);
$this->commandTester->execute(['command' => self::ADMIN_USER_CHANGE_PASSWORD]);
}
/**
* @Then I should be informed that password has been changed successfully
*/
public function iShouldBeInformedThatPasswordHasBeenChangedSuccessfully(): void
{
Assert::contains($this->commandTester->getDisplay(), 'Admin user password has been changed successfully.');
}
/**
* @Then I should be able to log in as :email authenticated by :password password
*/
public function iShouldBeAbleToLoginWithEmailAndPassword(string $email = '', string $password = ''): void
{
/** @var AdminUserInterface|null $adminUser */
$adminUser = $this->adminUserRepository->findOneByEmail($email);
$adminUser->setPlainPassword($password);
Assert::same($adminUser->getPassword(), $this->userPasswordHasher->hash($adminUser));
}
}

View file

@ -0,0 +1,38 @@
<?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\Transform;
use Behat\Behat\Context\Context;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Webmozart\Assert\Assert;
final class ShopUserContext implements Context
{
public function __construct(private UserRepositoryInterface $shopUserRepository)
{
}
/**
* @Transform :shopUser
*/
public function getShopUserByEmail(string $email): ShopUserInterface
{
$shopUser = $this->shopUserRepository->findOneByEmail($email);
Assert::notNull($shopUser, sprintf('Shop User with email "%s" does not exist', $email));
return $shopUser;
}
}

View file

@ -90,7 +90,7 @@ final class DashboardContext implements Context
}
/**
* @Then I should see :number new orders
* @Then I should see :number paid orders
*/
public function iShouldSeeNewOrders(int $number): void
{

View file

@ -222,7 +222,6 @@
<service id="sylius.behat.context.api.admin.managing_orders" class="Sylius\Behat\Context\Api\Admin\ManagingOrdersContext">
<argument type="service" id="sylius.behat.api_platform_client.admin" />
<argument type="service" id="Sylius\Behat\Client\ResponseCheckerInterface" />
<argument type="service" id="sylius.behat.request_factory" />
<argument type="service" id="api_platform.iri_converter" />
<argument type="service" id="sylius.behat.api_security" />
<argument type="service" id="sylius.behat.shared_storage" />
@ -303,6 +302,7 @@
<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.shared_storage" />
</service>
<service id="sylius.behat.context.api.admin.managing_placed_order_addresses" class="Sylius\Behat\Context\Api\Admin\ManagingPlacedOrderAddressesContext">
@ -317,5 +317,11 @@
<argument type="service" id="Sylius\Behat\Client\ResponseCheckerInterface" />
<argument type="service" id="sylius.behat.section_iri_converter" />
</service>
<service id="Sylius\Behat\Context\Api\Admin\DashboardContext">
<argument type="service" id="sylius.behat.api_platform_client.admin" />
<argument type="service" id="Sylius\Behat\Client\ResponseCheckerInterface" />
<argument type="service" id="Sylius\Calendar\Provider\DateTimeProviderInterface" />
</service>
</services>
</container>

View file

@ -27,5 +27,11 @@
<argument type="service" id="kernel" />
<argument type="service" id="sylius.repository.order" />
</service>
<service id="sylius.behat.context.cli.change_admin_password" class="Sylius\Behat\Context\Cli\ChangeAdminPasswordContext">
<argument type="service" id="kernel" />
<argument type="service" id="sylius.repository.admin_user" />
<argument type="service" id="sylius.security.password_hasher" />
</service>
</services>
</container>

View file

@ -179,5 +179,9 @@
<argument type="service" id="sylius.repository.zone" />
<argument type="service" id="sylius.repository.zone_member" />
</service>
<service id="sylius.behat.context.transform.shop_user" class="Sylius\Behat\Context\Transform\ShopUserContext">
<argument type="service" id="sylius.repository.shop_user" />
</service>
</services>
</container>

View file

@ -9,6 +9,7 @@ imports:
- suites/api/account/login.yml
- suites/api/addressing/managing_countries.yml
- suites/api/addressing/managing_zones.yml
- suites/api/admin/dashboard.yaml
- suites/api/admin/login.yml
- suites/api/admin/security.yml
- suites/api/cart/accessing_cart.yml
@ -68,8 +69,10 @@ imports:
- suites/api/user/managing_administrators.yml
- suites/api/user/managing_customer_groups.yml
- suites/api/user/managing_customers.yml
- suites/api/user/managing_users.yml
- suites/cli/canceling_unpaid_orders.yml
- suites/cli/change_admin_password.yml
- suites/cli/installer.yml
- suites/cli/showing_available_plugins.yml

View file

@ -0,0 +1,39 @@
# This file is part of the Sylius package.
# (c) Sylius Sp. z o.o.
default:
suites:
api_dashboard:
contexts:
- sylius.behat.context.hook.doctrine_orm
- Sylius\Calendar\Tests\Behat\Context\Hook\CalendarContext
- sylius.behat.context.transform.admin
- sylius.behat.context.transform.shared_storage
- sylius.behat.context.transform.user
- Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext
- sylius.behat.context.setup.admin_api_security
- sylius.behat.context.setup.channel
- sylius.behat.context.setup.currency
- sylius.behat.context.setup.geographical
- sylius.behat.context.setup.order
- sylius.behat.context.setup.payment
- sylius.behat.context.setup.product
- sylius.behat.context.setup.promotion
- sylius.behat.context.setup.shipping
- sylius.behat.context.setup.zone
- sylius.behat.context.transform.channel
- sylius.behat.context.transform.customer
- sylius.behat.context.transform.lexical
- sylius.behat.context.transform.order
- sylius.behat.context.transform.product
- sylius.behat.context.transform.shared_storage
- Sylius\Behat\Context\Api\Admin\DashboardContext
- sylius.behat.context.api.admin.login
filters:
tags: "@admin_dashboard&&@api"

View file

@ -0,0 +1,25 @@
# This file is part of the Sylius package.
# (c) Sylius Sp. z o.o.
default:
suites:
api_managing_users:
contexts:
- sylius.behat.context.hook.doctrine_orm
- sylius.behat.context.transform.customer
- sylius.behat.context.transform.shared_storage
- sylius.behat.context.transform.shop_user
- sylius.behat.context.setup.admin_api_security
- sylius.behat.context.setup.channel
- sylius.behat.context.setup.customer
- sylius.behat.context.setup.user
- sylius.behat.context.api.admin.managing_customers
- sylius.behat.context.api.admin.response
- sylius.behat.context.api.shop.login
filters:
tags: "@managing_users&&@api"

View file

@ -0,0 +1,15 @@
# This file is part of the Sylius package.
# (c) Sylius Sp. z o.o.
default:
suites:
change_admin_password:
contexts:
- sylius.behat.context.hook.doctrine_orm
- sylius.behat.context.setup.admin_user
- sylius.behat.context.cli.change_admin_password
filters:
tags: "@change_admin_password&&@cli"

View file

@ -7,7 +7,14 @@ default:
contexts:
- sylius.behat.context.hook.doctrine_orm
- sylius.behat.context.hook.session
- sylius.behat.context.transform.channel
- sylius.behat.context.transform.customer
- sylius.behat.context.transform.lexical
- sylius.behat.context.transform.order
- sylius.behat.context.transform.product
- sylius.behat.context.transform.shared_storage
- sylius.behat.context.setup.admin_security
- sylius.behat.context.setup.calendar
- sylius.behat.context.setup.channel
- sylius.behat.context.setup.currency
@ -16,18 +23,9 @@ default:
- sylius.behat.context.setup.payment
- sylius.behat.context.setup.product
- sylius.behat.context.setup.promotion
- sylius.behat.context.setup.admin_security
- sylius.behat.context.setup.shipping
- sylius.behat.context.setup.zone
- sylius.behat.context.transform.channel
- sylius.behat.context.transform.customer
- sylius.behat.context.transform.lexical
- sylius.behat.context.transform.order
- sylius.behat.context.transform.product
- sylius.behat.context.transform.shared_storage
- sylius.behat.context.ui.admin.dashboard
- sylius.behat.context.ui.admin.login
- sylius.behat.context.ui.admin.notification

View file

@ -13,7 +13,7 @@ declare(strict_types=1);
namespace Sylius\Bundle\AdminBundle\Action;
use Sylius\Bundle\AdminBundle\EmailManager\OrderEmailManagerInterface;
use Sylius\Bundle\CoreBundle\MessageDispatcher\ResendOrderConfirmationEmailDispatcherInterface;
use Sylius\Bundle\CoreBundle\Provider\FlashBagProvider;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
@ -30,7 +30,7 @@ final readonly class ResendOrderConfirmationEmailAction
{
public function __construct(
private OrderRepositoryInterface $orderRepository,
private OrderEmailManagerInterface $orderEmailManager,
private ResendOrderConfirmationEmailDispatcherInterface $resendOrderConfirmationEmailDispatcher,
private CsrfTokenManagerInterface $csrfTokenManager,
private RequestStack $requestStack,
) {
@ -52,7 +52,7 @@ final readonly class ResendOrderConfirmationEmailAction
throw new NotFoundHttpException(sprintf('The order with id %s has not been found', $orderId));
}
$this->orderEmailManager->sendConfirmationEmail($order);
$this->resendOrderConfirmationEmailDispatcher->dispatch($order);
FlashBagProvider
::getFlashBag($this->requestStack)

View file

@ -0,0 +1,78 @@
<?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\AdminBundle\Console\Command;
use Sylius\Bundle\AdminBundle\Console\Command\Factory\QuestionFactoryInterface;
use Sylius\Component\Core\Model\AdminUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Sylius\Component\User\Security\PasswordUpdaterInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'sylius:admin-user:change-password',
description: 'Change password of admin user',
)]
final class ChangeAdminUserPasswordCommand extends Command
{
protected SymfonyStyle $io;
/** @param UserRepositoryInterface<AdminUserInterface> $adminUserRepository */
public function __construct(
private UserRepositoryInterface $adminUserRepository,
private PasswordUpdaterInterface $passwordUpdater,
private QuestionFactoryInterface $questionFactory,
) {
parent::__construct();
}
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!$input->isInteractive()) {
$this->io->error('This command must be run interactively.');
return Command::FAILURE;
}
$this->io->title('Change admin user password');
$email = $this->io->askQuestion($this->questionFactory->createEmail());
/** @var AdminUserInterface|null $adminUser */
$adminUser = $this->adminUserRepository->findOneByEmail($email);
if ($adminUser === null) {
$this->io->error(sprintf('Admin user with email address %s not found!', $email));
return Command::INVALID;
}
$password = $this->io->askQuestion($this->questionFactory->createWithNotNullValidator('New password', true));
$adminUser->setPlainPassword($password);
$this->passwordUpdater->updatePassword($adminUser);
$this->adminUserRepository->add($adminUser);
$this->io->success('Admin user password has been changed successfully.');
return Command::SUCCESS;
}
}

View file

@ -11,15 +11,15 @@
declare(strict_types=1);
namespace Sylius\Bundle\AdminBundle\Command;
namespace Sylius\Bundle\AdminBundle\Console\Command;
use Sylius\Bundle\AdminBundle\Exception\CreateAdminUserFailedException;
use Sylius\Bundle\AdminBundle\Console\Command\Factory\QuestionFactoryInterface;
use Sylius\Bundle\AdminBundle\Message\CreateAdminUser;
use Sylius\Bundle\AdminBundle\Exception\CreateAdminUserFailedException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Intl\Locales;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
@ -34,10 +34,13 @@ final class CreateAdminUserCommand extends Command
{
use HandleTrait;
private SymfonyStyle $io;
protected SymfonyStyle $io;
public function __construct(MessageBusInterface $messageBus, private string $defaultLocaleCode)
{
public function __construct(
MessageBusInterface $messageBus,
private string $defaultLocaleCode,
private QuestionFactoryInterface $questionFactory,
) {
$this->messageBus = $messageBus;
parent::__construct();
@ -85,18 +88,19 @@ final class CreateAdminUserCommand extends Command
return Command::SUCCESS;
}
/** @return array<array-key, mixed> */
private function askAdminUserData(): array
{
$adminUserData = [];
$adminUserData['email'] = $this->io->askQuestion($this->createEmailQuestion());
$adminUserData['email'] = $this->io->askQuestion($this->questionFactory->createEmail());
$adminUserData['username'] = $this->io->askQuestion(
$this->createQuestionWithNonBlankValidator('Username'),
$this->questionFactory->createWithNotNullValidator('Username'),
);
$adminUserData['first_name'] = $this->io->ask('First name');
$adminUserData['last_name'] = $this->io->ask('Last name');
$adminUserData['plain_password'] = $this->io->askQuestion(
$this->createQuestionWithNonBlankValidator('Password', true),
$this->questionFactory->createWithNotNullValidator('Password', true),
);
$localeCodes = Locales::getNames();
@ -107,40 +111,7 @@ final class CreateAdminUserCommand extends Command
return $adminUserData;
}
private function createEmailQuestion(): Question
{
$question = new Question('Email');
$question->setValidator(function (?string $email) {
if (!filter_var($email, \FILTER_VALIDATE_EMAIL) || $email === null) {
throw new \InvalidArgumentException('The email address provided is invalid. Please try again.');
}
return $email;
});
$question->setMaxAttempts(3);
return $question;
}
private function createQuestionWithNonBlankValidator(string $askedQuestion, bool $hidden = false): Question
{
$question = new Question($askedQuestion);
$question->setValidator(function (?string $value) {
if ($value === null) {
throw new \InvalidArgumentException('The value cannot be empty.');
}
return $value;
});
$question->setMaxAttempts(3);
if ($hidden) {
$question->setHidden(true);
}
return $question;
}
/** @param array<array-key, mixed> $adminUserData */
private function showSummary(array $adminUserData): void
{
$this->io->writeln('The following admin user will be created:');

View file

@ -0,0 +1,50 @@
<?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\AdminBundle\Console\Command\Factory;
use Symfony\Component\Console\Question\Question;
final class QuestionFactory implements QuestionFactoryInterface
{
public function createEmail(): Question
{
$question = new Question('Email');
$question->setValidator(function (?string $email) {
if ($email === null || !filter_var($email, \FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('The email address provided is invalid. Please try again.');
}
return $email;
});
$question->setMaxAttempts(3);
return $question;
}
public function createWithNotNullValidator(string $askedQuestion, bool $hidden = false): Question
{
$question = new Question($askedQuestion);
$question->setValidator(function (?string $value) {
if ($value === null) {
throw new \InvalidArgumentException('The value cannot be empty.');
}
return $value;
});
$question->setMaxAttempts(3);
$question->setHidden($hidden);
return $question;
}
}

View file

@ -0,0 +1,23 @@
<?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\AdminBundle\Console\Command\Factory;
use Symfony\Component\Console\Question\Question;
interface QuestionFactoryInterface
{
public function createEmail(): Question;
public function createWithNotNullValidator(string $askedQuestion, bool $hidden = false): Question;
}

View file

@ -18,6 +18,15 @@ use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Mailer\Sender\SenderInterface;
use Webmozart\Assert\Assert;
trigger_deprecation(
'sylius/admin-bundle',
'1.13',
'The "%s" class is deprecated, use "%s" instead.',
OrderEmailManager::class,
\Sylius\Bundle\CoreBundle\EmailManager\OrderEmailManager::class,
);
/** @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see \Sylius\Bundle\CoreBundle\Mailer\OrderEmailManager} instead. */
final class OrderEmailManager implements OrderEmailManagerInterface
{
public function __construct(private SenderInterface $emailSender)

View file

@ -25,6 +25,11 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
final class CreateAdminUserHandler implements MessageHandlerInterface
{
/**
* @param UserRepositoryInterface<AdminUserInterface> $adminUserRepository
* @param FactoryInterface<AdminUserInterface> $adminUserFactory
* @param array<array-key, string> $validationGroups
*/
public function __construct(
private UserRepositoryInterface $adminUserRepository,
private FactoryInterface $adminUserFactory,
@ -65,6 +70,7 @@ final class CreateAdminUserHandler implements MessageHandlerInterface
return $adminUser;
}
/** @return iterable<string> */
private function getViolationMessages(ConstraintViolationListInterface $constraintViolationList): iterable
{
foreach ($constraintViolationList as $violation) {

View file

@ -14,7 +14,6 @@ declare(strict_types=1);
namespace Sylius\Bundle\AdminBundle\Provider;
use Sylius\Bundle\MoneyBundle\Formatter\MoneyFormatterInterface;
use Sylius\Component\Core\Dashboard\DashboardStatistics;
use Sylius\Component\Core\Dashboard\DashboardStatisticsProviderInterface;
use Sylius\Component\Core\Dashboard\Interval;
use Sylius\Component\Core\Dashboard\SalesDataProviderInterface;
@ -31,7 +30,6 @@ class StatisticsDataProvider implements StatisticsDataProviderInterface
public function getRawData(ChannelInterface $channel, \DateTimeInterface $startDate, \DateTimeInterface $endDate, string $interval): array
{
/** @var DashboardStatistics $statistics */
$statistics = $this->statisticsProvider->getStatisticsForChannelInPeriod($channel, $startDate, $endDate);
$salesSummary = $this->salesDataProvider->getSalesSummary(

View file

@ -17,5 +17,6 @@ use Sylius\Component\Core\Model\ChannelInterface;
interface StatisticsDataProviderInterface
{
/** @return array<array-key, array<array-key, mixed>> */
public function getRawData(ChannelInterface $channel, \DateTimeInterface $startDate, \DateTimeInterface $endDate, string $interval): array;
}

View file

@ -35,9 +35,17 @@
<services>
<defaults public="true" />
<service id="Sylius\Bundle\AdminBundle\Command\CreateAdminUserCommand">
<service id="Sylius\Bundle\AdminBundle\Console\Command\CreateAdminUserCommand">
<argument type="service" id="sylius.command_bus" />
<argument>%sylius_locale.locale%</argument>
<argument type="service" id="sylius.console.command_factory.question" />
<tag name="console.command" />
</service>
<service id="Sylius\Bundle\AdminBundle\Console\Command\ChangeAdminUserPasswordCommand">
<argument type="service" id="sylius.repository.admin_user" />
<argument type="service" id="sylius.security.password_updater" />
<argument type="service" id="sylius.console.command_factory.question" />
<tag name="console.command" />
</service>
@ -51,6 +59,10 @@
<tag name="messenger.message_handler" bus="sylius_default.bus" />
</service>
<service id="sylius.console.command_factory.question" class="Sylius\Bundle\AdminBundle\Console\Command\Factory\QuestionFactory" />
<service id="Sylius\Bundle\AdminBundle\Console\Command\Factory\QuestionFactoryInterface" alias="sylius.console.command_factory.question" />
<service id="sylius.context.locale.admin_based" class="Sylius\Bundle\AdminBundle\Context\AdminBasedLocaleContext">
<argument type="service" id="security.token_storage" />
<tag name="sylius.context.locale" priority="128" />

View file

@ -44,7 +44,7 @@
<service id="Sylius\Bundle\AdminBundle\Action\ResendOrderConfirmationEmailAction" public="true">
<argument type='service' id="sylius.repository.order" />
<argument type="service" id="Sylius\Bundle\AdminBundle\EmailManager\OrderEmailManagerInterface" />
<argument type="service" id="Sylius\Bundle\CoreBundle\MessageDispatcher\ResendOrderConfirmationEmailDispatcherInterface" />
<argument type="service" id="security.csrf.token_manager" />
<argument type="service" id="request_stack" />
</service>

View file

@ -0,0 +1,151 @@
<?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\AdminBundle\Tests\Console\Command;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Sylius\Bundle\AdminBundle\Console\Command\ChangeAdminUserPasswordCommand;
use Sylius\Bundle\AdminBundle\Console\Command\Factory\QuestionFactoryInterface;
use Sylius\Component\Core\Model\AdminUser;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Sylius\Component\User\Security\PasswordUpdaterInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Tester\CommandTester;
final class ChangeAdminUserPasswordCommandTest extends TestCase
{
private const EMAIL = 'sylius@example.com';
private const PASSWORD = 'Password';
private CommandTester $command;
private UserRepositoryInterface $userRepository;
private PasswordUpdaterInterface $passwordUpdater;
private QuestionFactoryInterface $questionFactory;
protected function setUp(): void
{
parent::setUp();
$this->userRepository = $this->createMock(UserRepositoryInterface::class);
$this->passwordUpdater = $this->createMock(PasswordUpdaterInterface::class);
$this->questionFactory = $this->createMock(QuestionFactoryInterface::class);
$this->command = new CommandTester(
new ChangeAdminUserPasswordCommand($this->userRepository, $this->passwordUpdater, $this->questionFactory),
);
}
/** @test */
public function it_does_not_execute_in_non_interactive_mode(): void
{
$this->command->execute([], ['interactive' => false]);
self::assertSame(Command::FAILURE, $this->command->getStatusCode());
}
/** @test */
public function it_does_not_change_password_when_admin_user_is_not_found(): void
{
$this
->questionFactory
->expects($this->once())
->method('createEmail')
->willReturn($this->createQuestionMock('Email'));
$this
->userRepository
->expects($this->once())
->method('findOneByEmail')
->willReturn(null);
$this
->command
->setInputs([
'email' => self::EMAIL,
]);
$this->command->execute([]);
self::assertSame(Command::INVALID, $this->command->getStatusCode());
}
/** @test */
public function it_changes_password_for_existing_admin_user(): void
{
$adminUser = $this->createMock(AdminUser::class);
$adminUser
->expects($this->once())
->method('setPlainPassword')
->with(self::PASSWORD);
$this
->questionFactory
->expects($this->once())
->method('createEmail')
->willReturn($this->createQuestionMock('Email'));
$this
->questionFactory
->expects($this->once())
->method('createWithNotNullValidator')
->with('New password', true)
->willReturn($this->createQuestionMock('New password'));
$this
->userRepository
->expects($this->once())
->method('findOneByEmail')
->willReturn($adminUser);
$this
->userRepository
->expects($this->once())
->method('add')
->with($adminUser);
$this
->passwordUpdater
->expects($this->once())
->method('updatePassword')
->with($adminUser);
$this
->command
->setInputs([
'email' => self::EMAIL,
'password' => self::PASSWORD,
]);
$this->command->execute([]);
self::assertSame(Command::SUCCESS, $this->command->getStatusCode());
}
private function createQuestionMock(string $askedQuestion): MockObject
{
$question = $this->createMock(Question::class);
$question
->method('isTrimmable')
->willReturn(true);
$question
->method('getQuestion')
->willReturn($askedQuestion);
return $question;
}
}

View file

@ -11,14 +11,16 @@
declare(strict_types=1);
namespace Sylius\Bundle\AdminBundle\Tests\Command;
namespace Sylius\Bundle\AdminBundle\Tests\Console\Command;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Sylius\Bundle\AdminBundle\Command\CreateAdminUserCommand;
use Sylius\Bundle\AdminBundle\Console\Command\Factory\QuestionFactoryInterface;
use Sylius\Bundle\AdminBundle\Console\Command\CreateAdminUserCommand;
use Sylius\Bundle\AdminBundle\Exception\CreateAdminUserFailedException;
use Sylius\Bundle\AdminBundle\Message\CreateAdminUser;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
@ -47,83 +49,52 @@ final class CreateAdminUserCommandTest extends TestCase
private MockObject $messageBus;
private QuestionFactoryInterface $questionFactory;
protected function setUp(): void
{
parent::setUp();
$this->messageBus = $this->createMock(MessageBusInterface::class);
$this->questionFactory = $this->createMock(QuestionFactoryInterface::class);
$this->command = new CommandTester(
new CreateAdminUserCommand($this->messageBus, self::LOCALE_CODE),
new CreateAdminUserCommand($this->messageBus, self::LOCALE_CODE, $this->questionFactory),
);
}
/** @test */
public function it_creates_an_admin_user_if_accepted_in_the_summary(): void
{
$adminUserData = $this->getDefaultAdminUserDataSetup();
$this
->questionFactory
->expects($this->once())
->method('createEmail')
->willReturn($this->createQuestionMock('Email'));
$commandInputs = $this->getDefaultCommandInputsSetup();
$this
->questionFactory
->expects($this->exactly(2))
->method('createWithNotNullValidator')
->willReturnOnConsecutiveCalls(
$this->createQuestionMock('Username'),
$this->createQuestionMock('New password'),
);
$this->assertSuccessfulCommandExecution($adminUserData, $commandInputs);
}
$this->command->setInputs($this->getDefaultCommandInputsSetup());
/** @test */
public function it_has_set_up_three_attempts_to_write_a_valid_email(): void
{
$adminUserData = $this->getDefaultAdminUserDataSetup();
$message = new CreateAdminUser(...array_values($this->getDefaultAdminUserDataSetup()));
$commandInputs = array_merge(
[
'first_email_entry' => 'invalid-email',
'second_email_entry' => 'still-invalid-email',
],
$this->getDefaultCommandInputsSetup(),
);
$this->messageBus->expects($this->once())
->method('dispatch')
->with($message)
->willReturn(new Envelope($message, [new HandledStamp(self::anything(), 'handler')]))
;
$this->assertSuccessfulCommandExecution($adminUserData, $commandInputs);
}
$this->command->execute([]);
/** @test */
public function it_has_set_up_three_attempts_to_write_a_non_blank_username(): void
{
$adminUserData = $this->getDefaultAdminUserDataSetup();
$commandInputs = [
'email' => self::EMAIL,
'first_username_entry' => '',
'second_username_entry' => '',
'username' => self::USERNAME,
'first_name' => self::FIRST_NAME,
'last_name' => self::LAST_NAME,
'password' => self::PASSWORD,
'locale_code' => self::LOCALE_CODE,
'admin_user_enabled' => self::YES,
'creation_confirmation' => self::YES,
];
$this->assertSuccessfulCommandExecution($adminUserData, $commandInputs);
}
/** @test */
public function it_has_set_up_three_attempts_to_write_a_non_blank_password(): void
{
$adminUserData = $this->getDefaultAdminUserDataSetup();
$commandInputs = [
'email' => self::EMAIL,
'username' => self::USERNAME,
'first_name' => self::FIRST_NAME,
'last_name' => self::LAST_NAME,
'first_password_entry' => '',
'second_password_entry' => '',
'password' => self::PASSWORD,
'locale_code' => self::LOCALE_CODE,
'admin_user_enabled' => self::YES,
'creation_confirmation' => self::YES,
];
$this->assertSuccessfulCommandExecution($adminUserData, $commandInputs);
$this->command->assertCommandIsSuccessful();
self::assertStringContainsString('Admin user has been successfully created.', $this->command->getDisplay());
}
/** @test */
@ -140,6 +111,21 @@ final class CreateAdminUserCommandTest extends TestCase
'creation_confirmation' => self::NO,
]);
$this
->questionFactory
->expects($this->once())
->method('createEmail')
->willReturn($this->createQuestionMock('Email'));
$this
->questionFactory
->expects($this->exactly(2))
->method('createWithNotNullValidator')
->willReturnOnConsecutiveCalls(
$this->createQuestionMock('Username'),
$this->createQuestionMock('New password'),
);
$this->messageBus->expects($this->never())->method('dispatch');
self::assertSame(Command::INVALID, $this->command->execute([]));
@ -155,6 +141,21 @@ final class CreateAdminUserCommandTest extends TestCase
$message = new CreateAdminUser(...array_values($adminUserData));
$this
->questionFactory
->expects($this->once())
->method('createEmail')
->willReturn($this->createQuestionMock('Email'));
$this
->questionFactory
->expects($this->exactly(2))
->method('createWithNotNullValidator')
->willReturnOnConsecutiveCalls(
$this->createQuestionMock('Username'),
$this->createQuestionMock('New password'),
);
$this->messageBus->expects($this->once())
->method('dispatch')
->with($message)
@ -175,24 +176,6 @@ final class CreateAdminUserCommandTest extends TestCase
self::assertSame(Command::FAILURE, $this->command->execute([], ['interactive' => false]));
}
private function assertSuccessfulCommandExecution(array $adminUserData, array $commandInputs): void
{
$this->command->setInputs($commandInputs);
$message = new CreateAdminUser(...array_values($adminUserData));
$this->messageBus->expects($this->once())
->method('dispatch')
->with($message)
->willReturn(new Envelope($message, [new HandledStamp(self::anything(), 'handler')]))
;
$this->command->execute([]);
$this->command->assertCommandIsSuccessful();
self::assertStringContainsString('Admin user has been successfully created.', $this->command->getDisplay());
}
private function getDefaultCommandInputsSetup(): array
{
return [
@ -219,4 +202,17 @@ final class CreateAdminUserCommandTest extends TestCase
'admin_user_enabled' => true,
];
}
private function createQuestionMock(string $askedQuestion): MockObject
{
$question = $this->createMock(Question::class);
$question
->method('isTrimmable')
->willReturn(true);
$question
->method('getQuestion')
->willReturn($askedQuestion);
return $question;
}
}

View file

@ -0,0 +1,78 @@
<?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\AdminBundle\Tests\Console\Command\Factory;
use PHPUnit\Framework\TestCase;
use Sylius\Bundle\AdminBundle\Console\Command\Factory\QuestionFactory;
final class QuestionFactoryTest extends TestCase
{
/** @test */
public function it_creates_email_question(): void
{
$questionFactory = new QuestionFactory();
$question = $questionFactory->createEmail();
$this->assertSame('Email', $question->getQuestion());
$this->assertSame(3, $question->getMaxAttempts());
$this->assertEquals('test@example.com', $question->getValidator()('test@example.com'));
}
/** @test */
public function it_creates_email_question_with_invalid_email(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The email address provided is invalid. Please try again.');
$questionFactory = new QuestionFactory();
$emailQuestion = $questionFactory->createEmail();
$emailQuestion->getValidator()('invalid-email');
}
/** @test */
public function it_creates_email_question_with_null_email(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The email address provided is invalid. Please try again.');
$questionFactory = new QuestionFactory();
$emailQuestion = $questionFactory->createEmail();
$emailQuestion->getValidator()(null);
}
/** @test */
public function it_creates_question_with_not_null_validator(): void
{
$questionFactory = new QuestionFactory();
$question = $questionFactory->createWithNotNullValidator('Question');
$this->assertSame('Question', $question->getQuestion());
$this->assertSame(3, $question->getMaxAttempts());
$this->assertEquals('test', $question->getValidator()('test'));
}
/** @test */
public function it_creates_question_with_not_null_validator_with_null_value(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The value cannot be empty.');
$questionFactory = new QuestionFactory();
$question = $questionFactory->createWithNotNullValidator('Question');
$question->getValidator()(null);
}
}

View file

@ -17,6 +17,7 @@ return [
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Sylius\Abstraction\StateMachine\SyliusStateMachineAbstractionBundle::class => ['all' => true],
Sylius\Bundle\OrderBundle\SyliusOrderBundle::class => ['all' => true],
Sylius\Bundle\MoneyBundle\SyliusMoneyBundle::class => ['all' => true],
Sylius\Bundle\CurrencyBundle\SyliusCurrencyBundle::class => ['all' => true],

View file

@ -0,0 +1,34 @@
<?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\Command\Customer;
use Sylius\Bundle\ApiBundle\Command\ShopUserIdAwareInterface;
/** @experimental */
class RemoveShopUser implements ShopUserIdAwareInterface
{
public function __construct(private mixed $shopUserId)
{
}
public function getShopUserId(): mixed
{
return $this->shopUserId;
}
public function setShopUserId(mixed $shopUserId): void
{
$this->shopUserId = $shopUserId;
}
}

View file

@ -0,0 +1,20 @@
<?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\Command;
use Sylius\Bundle\CoreBundle\Message\ResendOrderConfirmationEmail as BaseResendOrderConfirmationEmail;
class ResendOrderConfirmationEmail extends BaseResendOrderConfirmationEmail implements OrderTokenValueAwareInterface
{
}

View file

@ -0,0 +1,43 @@
<?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\CommandHandler\Customer;
use Sylius\Bundle\ApiBundle\Command\Customer\RemoveShopUser;
use Sylius\Bundle\ApiBundle\Exception\UserNotFoundException;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
/** @experimental */
final class RemoveShopUserHandler
{
/**
* @param UserRepositoryInterface<ShopUserInterface> $shopUserRepository
*/
public function __construct(
private UserRepositoryInterface $shopUserRepository,
){
}
public function __invoke(RemoveShopUser $removeShopUser): void
{
$shopUser = $this->shopUserRepository->find($removeShopUser->getShopUserId());
if (null === $shopUser) {
throw new UserNotFoundException();
}
$shopUser->setCustomer(null);
$this->shopUserRepository->remove($shopUser);
}
}

View file

@ -0,0 +1,99 @@
<?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\Controller;
use Sylius\Bundle\ApiBundle\Exception\ChannelNotFoundException;
use Sylius\Bundle\ApiBundle\Query\GetStatistics;
use Sylius\Bundle\ApiBundle\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\HandleTrait;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Constraints as BaseAssert;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
final class GetStatisticsAction
{
use HandleTrait;
private BaseAssert\Collection $constraint;
public function __construct(
MessageBusInterface $messageBus,
private SerializerInterface $serializer,
private ValidatorInterface $validator,
) {
$this->messageBus = $messageBus;
$this->constraint = new BaseAssert\Collection([
'channelCode' => new Assert\Code(),
'startDate' => new BaseAssert\DateTime('Y-m-d\TH:i:s', message: 'sylius.date_time.invalid'),
'dateInterval' => new Assert\DateInterval(),
'endDate' => new BaseAssert\DateTime('Y-m-d\TH:i:s', message: 'sylius.date_time.invalid'),
]);
}
/**
* @throws \Throwable
*/
public function __invoke(Request $request): Response
{
$parameters = $request->query->all();
$violations = $this->validator->validate($parameters, $this->constraint);
if (count($violations) > 0) {
return $this->createBadRequestResponse($violations);
}
$period = new \DatePeriod(
new \DateTimeImmutable($parameters['startDate']),
new \DateInterval($parameters['dateInterval']),
new \DateTimeImmutable($parameters['endDate']),
);
$violations = $this->validator->validate($period, new Assert\DatePeriod());
if (count($violations) > 0) {
return $this->createBadRequestResponse($violations);
}
try {
$result = $this->handle(new GetStatistics($period, $parameters['channelCode']));
$status = Response::HTTP_OK;
} catch (HandlerFailedException $exception) {
$exception = $exception->getPrevious();
$result = ['message' => $exception->getMessage()];
if ($exception instanceof ChannelNotFoundException) {
$status = Response::HTTP_NOT_FOUND;
} else {
throw $exception;
}
}
return new JsonResponse(data: $this->serializer->serialize($result, 'json'), status: $status, json: true);
}
private function createBadRequestResponse(ConstraintViolationListInterface $violations): JsonResponse
{
return new JsonResponse(
data: $this->serializer->serialize($violations, 'json'),
status: Response::HTTP_BAD_REQUEST,
json: true,
);
}
}

View file

@ -0,0 +1,51 @@
<?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\Controller;
use Sylius\Bundle\ApiBundle\Command\Customer\RemoveShopUser;
use Sylius\Bundle\ApiBundle\Exception\CustomerNotFoundException;
use Sylius\Bundle\ApiBundle\Exception\UserNotFoundException;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
/** @experimental */
final class RemoveCustomerShopUserAction
{
/**
* @param UserRepositoryInterface<ShopUserInterface> $shopUserRepository
*/
public function __construct(
private MessageBusInterface $commandBus,
private UserRepositoryInterface $shopUserRepository,
) {
}
public function __invoke(Request $request): Response
{
$customerId = $request->attributes->get('id', '');
$user = $this->shopUserRepository->findOneBy(['customer' => $customerId]);
if (null === $user) {
throw new UserNotFoundException();
}
$this->commandBus->dispatch(new RemoveShopUser($user->getId()));
return new JsonResponse(status: Response::HTTP_NO_CONTENT);
}
}

View file

@ -0,0 +1,54 @@
<?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 Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\User\Security\PasswordUpdaterInterface;
/** @experimental */
final class CustomerDataPersister implements ContextAwareDataPersisterInterface
{
public function __construct(
private ContextAwareDataPersisterInterface $decoratedDataPersister,
private PasswordUpdaterInterface $passwordUpdater,
) {
}
/** @param array<string, mixed> $context */
public function supports($data, array $context = []): bool
{
return $data instanceof CustomerInterface;
}
/**
* @param CustomerInterface $data
* @param array<string, mixed> $context
*/
public function persist($data, array $context = []): void
{
$user = $data->getUser();
if (null !== $user && null !== $user->getPlainPassword()) {
$this->passwordUpdater->updatePassword($user);
}
$this->decoratedDataPersister->persist($data, $context);
}
/** @param array<string, mixed> $context */
public function remove($data, array $context = []): void
{
$this->decoratedDataPersister->remove($data, $context);
}
}

View file

@ -14,7 +14,7 @@ declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use Sylius\Bundle\LocaleBundle\Checker\Exception\LocaleIsUsedException;
use Sylius\Bundle\ApiBundle\Exception\LocaleIsUsedException;
use Sylius\Bundle\LocaleBundle\Checker\LocaleUsageCheckerInterface;
use Sylius\Component\Locale\Model\LocaleInterface;

View file

@ -0,0 +1,28 @@
<?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;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/** @experimental */
final class ChannelNotFoundException extends NotFoundHttpException
{
public function __construct(
string $message = 'Channel not found.',
\Throwable $previous = null,
int $code = 0,
) {
parent::__construct($message, $previous, $code);
}
}

View file

@ -11,7 +11,7 @@
declare(strict_types=1);
namespace Sylius\Bundle\LocaleBundle\Checker\Exception;
namespace Sylius\Bundle\ApiBundle\Exception;
use Exception;

View file

@ -0,0 +1,30 @@
<?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;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/** @experimental */
final class UserNotFoundException extends NotFoundHttpException
{
/** @param array<array-key, mixed> $headers */
public function __construct(
string $message = 'User not found.',
\Throwable $previous = null,
int $code = 0,
array $headers = [],
) {
parent::__construct($message, $previous, $code, $headers);
}
}

View file

@ -0,0 +1,167 @@
<?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\OpenApi\Documentation;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\OpenApi\Model\Parameter;
use ApiPlatform\OpenApi\Model\PathItem;
use ApiPlatform\OpenApi\OpenApi;
use Sylius\Calendar\Provider\DateTimeProviderInterface;
use Symfony\Component\HttpFoundation\Response;
/** @experimental */
final class StatisticsModifier implements DocumentationModifierInterface
{
private const PATH = '/admin/statistics';
public function __construct(
private string $apiRoute,
private DateTimeProviderInterface $dateTimeProvider,
) {
}
public function modify(OpenApi $docs): OpenApi
{
$schemas = $docs->getComponents()->getSchemas();
$schemas['Statistics'] = [
'type' => 'object',
'properties' => [
'salesPerPeriod' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'period' => [
'type' => 'string',
'format' => 'date-time',
],
'total' => [
'type' => 'integer',
'example' => '1000',
],
],
],
'minItems' => 12,
],
'newCustomersCount' => [
'type' => 'integer',
'example' => '10',
],
'paidOrdersCount' => [
'type' => 'integer',
'example' => '12',
],
'averageOrderValue' => [
'type' => 'integer',
'example' => '1000',
],
'totalSales' => [
'type' => 'integer',
'example' => '12000',
],
'intervalType' => [
'type' => 'string',
'enum' => ['month', 'year', 'week', 'day'],
],
],
];
$path = $this->apiRoute . self::PATH;
$paths = $docs->getPaths();
$paths->addPath($path, $this->getPathItem());
return $docs
->withPaths($paths)
->withComponents($docs->getComponents()->withSchemas($schemas))
;
}
private function getPathItem(): PathItem
{
return new PathItem(
ref: 'Statistics',
summary: 'Get statistics',
get: new Operation(
operationId: 'get_statistics',
tags: ['Statistics'],
responses: [
Response::HTTP_OK => [
'description' => 'Statistics',
'content' => [
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/Statistics',
],
],
],
],
],
summary: 'Get statistics',
description: 'Get statistics',
parameters: $this->getParameters(),
),
);
}
/** @return Parameter[] */
private function getParameters(): array
{
$channelCode = new Parameter(
name: 'channelCode',
in: 'query',
description: 'Channel to get statistics for',
required: true,
schema: [
'type' => 'string',
],
);
$startDate = new Parameter(
name: 'startDate',
in: 'query',
description: 'Start date for statistics',
required: true,
schema: [
'type' => 'string',
'format' => 'date-time',
'default' => $this->dateTimeProvider->now()->format('Y-01-01\T00:00:00'),
],
);
$dateInterval = new Parameter(
name: 'dateInterval',
in: 'query',
description: 'Date interval for statistics',
required: true,
schema: [
'type' => 'string',
'default' => 'P1M',
],
);
$endDate = new Parameter(
name: 'endDate',
in: 'query',
description: 'End date for statistics',
required: true,
schema: [
'type' => 'string',
'format' => 'date-time',
'default' => $this->dateTimeProvider->now()->format('Y-12-31\T23:59:59'),
],
);
return [$channelCode, $startDate, $dateInterval, $endDate];
}
}

View file

@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Query;
/** @experimental */
class GetStatistics
{
public function __construct(private \DatePeriod $datePeriod, private string $channelCode)
{
}
public function getDatePeriod(): \DatePeriod
{
return $this->datePeriod;
}
public function getChannelCode(): string
{
return $this->channelCode;
}
}

View file

@ -0,0 +1,46 @@
<?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\QueryHandler;
use Sylius\Bundle\ApiBundle\Exception\ChannelNotFoundException;
use Sylius\Bundle\ApiBundle\Query\GetStatistics;
use Sylius\Component\Channel\Repository\ChannelRepositoryInterface;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Statistics\Provider\StatisticsProviderInterface;
use Sylius\Component\Core\Statistics\ValueObject\Statistics;
/** @experimental */
final class GetStatisticsHandler
{
/** @param ChannelRepositoryInterface<ChannelInterface> $channelRepository */
public function __construct(
private StatisticsProviderInterface $statisticsProvider,
private ChannelRepositoryInterface $channelRepository,
) {
}
public function __invoke(GetStatistics $query): Statistics
{
/** @var ChannelInterface|null $channel */
$channel = $this->channelRepository->findOneByCode($query->getChannelCode());
if ($channel === null) {
throw new ChannelNotFoundException(
sprintf('Channel with code "%s" does not exist.', $query->getChannelCode()),
);
}
return $this->statisticsProvider->provide($query->getDatePeriod(), $channel);
}
}

View file

@ -95,6 +95,12 @@
</attribute>
</itemOperation>
<itemOperation name="admin_delete_user">
<attribute name="method">DELETE</attribute>
<attribute name="path">/admin/customer/{id}/user</attribute>
<attribute name="controller">Sylius\Bundle\ApiBundle\Controller\RemoveCustomerShopUserAction</attribute>
</itemOperation>
<itemOperation name="shop_get">
<attribute name="method">GET</attribute>
<attribute name="path">/shop/customers/{id}</attribute>

View file

@ -382,6 +382,18 @@
<attribute name="summary">Addresses cart to given location, logged in Customer does not have to provide an email. Applies coupon to cart.</attribute>
</attribute>
</itemOperation>
<itemOperation name="admin_resend_confirmation">
<attribute name="method">POST</attribute>
<attribute name="path">/admin/orders/{tokenValue}/resend-confirmation-email</attribute>
<attribute name="messenger">input</attribute>
<attribute name="input">Sylius\Bundle\ApiBundle\Command\ResendOrderConfirmationEmail</attribute>
<attribute name="output">false</attribute>
<attribute name="status">202</attribute>
<attribute name="openapi_context">
<attribute name="summary">Resends order confirmation email</attribute>
</attribute>
</itemOperation>
</itemOperations>
<subresourceOperations>

View file

@ -60,6 +60,7 @@ api_platform:
Sylius\Bundle\ApiBundle\Exception\ChannelPricingChannelCodeMismatchException: 422
Sylius\Bundle\ApiBundle\Exception\InvalidProductAttributeValueTypeException: 422
Sylius\Bundle\ApiBundle\Exception\InvalidRequestArgumentException: 400
Sylius\Bundle\ApiBundle\Exception\LocaleIsUsedException: 422
Sylius\Bundle\ApiBundle\Exception\OrderItemNotFoundException: 422
Sylius\Bundle\ApiBundle\Exception\OrderNoLongerEligibleForPromotion: 422
Sylius\Bundle\ApiBundle\Exception\PaymentMethodCannotBeRemoved: 422
@ -75,7 +76,6 @@ api_platform:
Sylius\Bundle\ApiBundle\Exception\ZoneCannotBeRemoved: 422
Sylius\Bundle\ApiBundle\Exception\CannotRemoveMenuTaxonException: 409
Sylius\Bundle\ApiBundle\Exception\PromotionCannotBeRemoved: 422
Sylius\Bundle\LocaleBundle\Checker\Exception\LocaleIsUsedException: 422
Sylius\Bundle\UserBundle\Exception\UserNotFoundException: 404
Sylius\Component\Promotion\Exception\FailedGenerationException: 422
Symfony\Component\Serializer\Exception\UnexpectedValueException: 400

View file

@ -80,6 +80,12 @@
<tag name="sylius.open_api.modifier" />
</service>
<service id="Sylius\Bundle\ApiBundle\OpenApi\Documentation\StatisticsModifier">
<argument>%sylius.security.new_api_route%</argument>
<argument type="service" id="Sylius\Calendar\Provider\DateTimeProviderInterface" />
<tag name="sylius.open_api.modifier" />
</service>
<service id="Sylius\Bundle\ApiBundle\OpenApi\Documentation\PromotionDocumentationModifier">
<argument>%sylius.security.new_api_route%</argument>
<argument>%sylius.promotion_actions%</argument>

View file

@ -10,6 +10,12 @@ sylius_api_admin_authentication_token:
path: /admin/authentication-token
methods: ['POST']
sylius_api_admin_statistics:
path: /admin/statistics
methods: ['GET']
defaults:
_controller: Sylius\Bundle\ApiBundle\Controller\GetStatisticsAction
sylius_api_shop_authentication_token:
path: /shop/authentication-token
methods: ['POST']

View file

@ -231,5 +231,11 @@
<tag name="messenger.message_handler" bus="sylius.command_bus" />
<tag name="messenger.message_handler" bus="sylius_default.bus" />
</service>
<service id="Sylius\Bundle\ApiBundle\CommandHandler\Customer\RemoveShopUserHandler">
<argument type="service" id="sylius.repository.shop_user" />
<tag name="messenger.message_handler" bus="sylius.command_bus" />
<tag name="messenger.message_handler" bus="sylius_default.bus" />
</service>
</services>
</container>

View file

@ -60,5 +60,15 @@
<tag name="controller.service_arguments" />
</service>
<service id="Sylius\Bundle\ApiBundle\Controller\RemoveCustomerShopUserAction">
<argument type="service" id="sylius.command_bus" />
<argument type="service" id="sylius.repository.shop_user" />
</service>
<service id="Sylius\Bundle\ApiBundle\Controller\GetStatisticsAction">
<argument type="service" id="sylius.query_bus" />
<argument type="service" id="serializer" />
<argument type="service" id="validator" />
</service>
</services>
</container>

View file

@ -105,5 +105,11 @@
<argument type="service" id="api_platform.doctrine.orm.data_persister" />
<tag name="api_platform.data_persister" />
</service>
<service id="Sylius\Bundle\ApiBundle\DataPersister\CustomerDataPersister">
<argument type="service" id="api_platform.doctrine.orm.data_persister" />
<argument type="service" id="sylius.security.password_updater" />
<tag name="api_platform.data_persister" />
</service>
</services>
</container>

View file

@ -25,6 +25,11 @@
<argument type="service" id="sylius.customer_statistics_provider" />
</service>
<service id="Sylius\Bundle\ApiBundle\QueryHandler\GetStatisticsHandler">
<argument type="service" id="Sylius\Component\Core\Statistics\Provider\StatisticsProviderInterface" />
<argument type="service" id="sylius.repository.channel" />
</service>
<service id="Sylius\Bundle\ApiBundle\QueryHandler\GetAddressLogEntryCollectionHandler">
<argument type="service" id="sylius.repository.address_log_entry" />
</service>

View file

@ -188,5 +188,17 @@
<argument type="service" id="Sylius\Bundle\ApiBundle\Validator\CatalogPromotion\ForVariantsScopeValidator.inner" />
<argument type="service" id="Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface" />
</service>
<service id="Sylius\Bundle\ApiBundle\Validator\Constraints\DateIntervalValidator">
<tag name="validator.constraint_validator" alias="sylius_api_validator_date_interval" />
</service>
<service id="Sylius\Bundle\ApiBundle\Validator\Constraints\EndDateIsNotBeforeStartDateValidator">
<tag name="validator.constraint_validator" alias="sylius_api_validator_date_period_end_date_is_not_before_start_date" />
</service>
<service id="Sylius\Bundle\ApiBundle\Validator\Constraints\EndDateAgainstIntervalValidator">
<tag name="validator.constraint_validator" alias="sylius_api_validator_end_date_against_interval" />
</service>
</services>
</container>

View file

@ -6,10 +6,20 @@ sylius:
is_verified: 'Account with email %email% is currently verified.'
address:
without_country: 'The address without country cannot exist'
code:
invalid: 'The code should contain only letters, numbers, dashes ("-" symbol) and underscores ("_" symbol).'
contact_request:
email_is_invalid: 'The provided email is invalid.'
country:
not_exist: 'The country %countryCode% does not exist.'
date_interval:
empty: 'The date interval cannot be empty.'
invalid: 'The date interval is not valid ISO 8601 interval.'
date_period:
end_date_is_not_before_start_date: 'The end date is not before start date.'
end_date_must_be_multiple_of_interval: 'End date "%givenDate%" must be multiple of interval, expected "%expectedDate%".'
date_time:
invalid: 'The date time is not valid ISO 8601 date time in Y-m-d\TH:i:s format.'
order:
not_empty: 'An empty order cannot be completed.'
payment_method:

View file

@ -16,6 +16,7 @@ return [
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Sylius\Abstraction\StateMachine\SyliusStateMachineAbstractionBundle::class => ['all' => true],
Sylius\Bundle\OrderBundle\SyliusOrderBundle::class => ['all' => true],
Sylius\Bundle\MoneyBundle\SyliusMoneyBundle::class => ['all' => true],
Sylius\Bundle\CurrencyBundle\SyliusCurrencyBundle::class => ['all' => true],

View file

@ -0,0 +1,35 @@
<?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\Constraints\Compound;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Component\Validator\Constraints\Type;
/** @experimental */
final class Code extends Compound
{
/**
* @param array<mixed> $options
*/
protected function getConstraints(array $options): array
{
return [
new NotBlank(message: 'sylius.code.not_blank'),
new Type('string'),
new Regex(['pattern' => '/^[\w-]*$/'], message: 'sylius.code.invalid'),
];
}
}

View file

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

View file

@ -0,0 +1,34 @@
<?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;
use Symfony\Component\Validator\ConstraintValidator;
use Webmozart\Assert\Assert;
/** @experimental */
final class DateIntervalValidator extends ConstraintValidator
{
public function validate(mixed $value, Constraint $constraint): void
{
Assert::string($value);
Assert::isInstanceOf($constraint, DateInterval::class);
try {
new \DateInterval($value);
} catch (\Exception) {
$this->context->addViolation($constraint->message);
}
}
}

View file

@ -0,0 +1,31 @@
<?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\Constraints\Compound;
/** @experimental */
final class DatePeriod extends Compound
{
/**
* @param array<mixed> $options
*/
protected function getConstraints(array $options): array
{
return [
new EndDateIsNotBeforeStartDate(),
new EndDateAgainstInterval(),
];
}
}

View file

@ -0,0 +1,33 @@
<?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 EndDateAgainstInterval extends Constraint
{
/** @var string */
public $message = 'sylius.date_period.end_date_must_be_multiple_of_interval';
public function validatedBy(): string
{
return 'sylius_api_validator_end_date_against_interval';
}
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}

View file

@ -0,0 +1,57 @@
<?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;
use Symfony\Component\Validator\ConstraintValidator;
use Webmozart\Assert\Assert;
/** @experimental */
final class EndDateAgainstIntervalValidator extends ConstraintValidator
{
/**
* @param \DatePeriod $value
* @param EndDateAgainstInterval $constraint
*
* Validates that the end date is a multiple of the interval.
* The end date is adjusted by subtracting one second to make it inclusive (closed interval).
* If the adjusted end date does not match the provided end date, an exception is thrown.
*/
public function validate(mixed $value, Constraint $constraint): void
{
Assert::isInstanceOf($value, \DatePeriod::class);
Assert::isInstanceOf($constraint, EndDateAgainstInterval::class);
$currentDate = clone $value->getStartDate();
$endDate = $value->getEndDate();
$interval = $value->getDateInterval();
while ($currentDate <= $endDate) {
$currentDate = $currentDate->add($interval);
}
/** We shift to make closed interval. */
$intervalEndDate = $currentDate->modify('-1 second');
if ($intervalEndDate != $endDate) {
$this
->context
->buildViolation($constraint->message)
->setParameter('%givenDate%', $endDate->format('Y-m-d H:i:s'))
->setParameter('%expectedDate%', $intervalEndDate->format('Y-m-d H:i:s'))
->addViolation()
;
}
}
}

View file

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

View file

@ -0,0 +1,36 @@
<?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;
use Symfony\Component\Validator\ConstraintValidator;
use Webmozart\Assert\Assert;
/** @experimental */
final class EndDateIsNotBeforeStartDateValidator extends ConstraintValidator
{
/**
* @param \DatePeriod $value
* @param EndDateIsNotBeforeStartDate $constraint
*/
public function validate(mixed $value, Constraint $constraint): void
{
Assert::isInstanceOf($value, \DatePeriod::class);
Assert::isInstanceOf($constraint, EndDateIsNotBeforeStartDate::class);
if ($value->getStartDate() > $value->getEndDate()) {
$this->context->addViolation($constraint->message);
}
}
}

View file

@ -0,0 +1,45 @@
<?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\CommandHandler;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Command\Customer\RemoveShopUser;
use Sylius\Bundle\UserBundle\Exception\UserNotFoundException;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
final class RemoveShopUserHandlerSpec extends ObjectBehavior
{
function let(UserRepositoryInterface $userRepository): void
{
$this->beConstructedWith($userRepository);
}
public function it_throws_an_exception_if_user_has_not_been_found(UserRepositoryInterface $userRepository): void
{
$userRepository->find(42)->willReturn(null);
$this->shouldThrow(UserNotFoundException::class)->during('__invoke', [new RemoveShopUser(42)]);
}
public function it_should_remove_shop_user(UserRepositoryInterface $userRepository, ShopUserInterface $shopUser): void
{
$userRepository->find(42)->willReturn($shopUser);
$shopUser->setCustomer(null)->shouldBeCalled();
$userRepository->remove($shopUser)->shouldBeCalled();
$this->__invoke(new RemoveShopUser(42));
}
}

View file

@ -0,0 +1,90 @@
<?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 PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\Component\Core\Model\ShopUserInterface;
use Sylius\Component\User\Security\PasswordUpdaterInterface;
final class CustomerDataPersisterSpec extends ObjectBehavior
{
function let(
ContextAwareDataPersisterInterface $decoratedDataPersister,
PasswordUpdaterInterface $passwordUpdater,
): void {
$this->beConstructedWith($decoratedDataPersister, $passwordUpdater);
}
function it_supports_only_customer_entity(CustomerInterface $customer, ProductInterface $product): void
{
$this->supports($customer)->shouldReturn(true);
$this->supports($product)->shouldReturn(false);
}
function it_does_not_update_password_when_user_is_null(
ContextAwareDataPersisterInterface $decoratedDataPersister,
PasswordUpdaterInterface $passwordUpdater,
CustomerInterface $customer,
): void {
$customer->getUser()->willReturn(null);
$passwordUpdater->updatePassword(Argument::any())->shouldNotBeCalled();
$decoratedDataPersister->persist($customer, [])->shouldBeCalled();
$this->persist($customer, []);
}
function it_does_not_update_password_when_plain_password_is_null(
ContextAwareDataPersisterInterface $decoratedDataPersister,
PasswordUpdaterInterface $passwordUpdater,
CustomerInterface $customer,
ShopUserInterface $user,
): void {
$user->getPlainPassword()->willReturn(null);
$customer->getUser()->willReturn($user);
$passwordUpdater->updatePassword($user)->shouldNotBeCalled();
$decoratedDataPersister->persist($customer, [])->shouldBeCalled();
$this->persist($customer, []);
}
function it_updates_password_when_plain_password_is_set(
ContextAwareDataPersisterInterface $decoratedDataPersister,
PasswordUpdaterInterface $passwordUpdater,
CustomerInterface $customer,
ShopUserInterface $user,
): void {
$user->getPlainPassword()->willReturn('password');
$customer->getUser()->willReturn($user);
$passwordUpdater->updatePassword($user)->shouldBeCalled();
$decoratedDataPersister->persist($customer, [])->shouldBeCalled();
$this->persist($customer, []);
}
function it_uses_decorated_data_persister_to_remove_customer(
ContextAwareDataPersisterInterface $decoratedDataPersister,
CustomerInterface $customer,
): void {
$decoratedDataPersister->remove($customer, [])->shouldBeCalled();
$this->remove($customer, []);
}
}

Some files were not shown because too many files have changed in this diff Show more