diff --git a/.github/workflows/ci_static-checks.yaml b/.github/workflows/ci_static-checks.yaml index ae4cc1ff79..0e67e16dc4 100644 --- a/.github/workflows/ci_static-checks.yaml +++ b/.github/workflows/ci_static-checks.yaml @@ -103,6 +103,7 @@ jobs: - name: "Check for security vulnerabilities" run: symfony security:check + continue-on-error: true - name: "Validate Twig templates" run: bin/console lint:twig src diff --git a/CONFLICTS.md b/CONFLICTS.md index ad952188fe..1a61604305 100644 --- a/CONFLICTS.md +++ b/CONFLICTS.md @@ -27,3 +27,7 @@ references related issues. - https://github.com/stof/StofDoctrineExtensionsBundle/issues/455 - https://github.com/doctrine-extensions/DoctrineExtensions/issues/2600 + +- `doctrine/doctrine-bundle:2.11.0`: + + This version uses the [readonly property](https://github.com/doctrine/DoctrineBundle/blob/2.11.0/Repository/ServiceEntityRepositoryProxy.php#L34), which is available from PHP 8.1 while the package also supports PHP 7.4 and 8.0, leading to compatibility issues. diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md index a763c6424b..46d3993a5c 100644 --- a/UPGRADE-2.0.md +++ b/UPGRADE-2.0.md @@ -3,7 +3,7 @@ ## Codebase * Doctrine MongoDB and PHPCR is not longer supported in ResourceBundle and GridBundle: - + * The following classes were removed: * `Sylius\Bundle\GridBundle\Doctrine\PHPCRODM\DataSource` @@ -26,13 +26,13 @@ * `Sylius\Bundle\ResourceBundle\EventListener\ODMTranslatableListener` * The following services were removed: - + * `sylius.event_subscriber.odm_mapped_super_class` * `sylius.event_subscriber.odm_repository_class` * `sylius.grid_driver.doctrine.phpcrodm` - + * The following parameters were removed: - + * `sylius.mongodb_odm.repository.class` * `sylius.phpcr_odm.repository.class` @@ -40,28 +40,33 @@ * `sylius.mailer.templates` -* Added the `Sylius\Component\Order\Context\ResettableCartContextInterface` that extends `Sylius\Component\Order\Context\CartContextInterface` and `Symfony\Contracts\Service\ResetInterface`. +* Added the `Sylius\Component\Order\Context\ResettableCartContextInterface` that + extends `Sylius\Component\Order\Context\CartContextInterface` and `Symfony\Contracts\Service\ResetInterface`. -* The name of the default `LiipImagineBundle`'s resolver and loader were changed from **default** to **sylius_image** ([reference](https://github.com/Sylius/Sylius/pull/12543)). - To change the default resolver and/or loader for `LiipImagineBundle`, configure `cache` and/or `data_loader` parameters under the `liip_imagine` key. +* The name of the default `LiipImagineBundle`'s resolver and loader were changed from **default** to **sylius_image + ** ([reference](https://github.com/Sylius/Sylius/pull/12543)). + To change the default resolver and/or loader for `LiipImagineBundle`, configure `cache` and/or `data_loader` + parameters under the `liip_imagine` key. -* The `sylius/calendar` package has been replaced with `symfony/clock` package. All usages of the `Sylius\Calendar\Provider\DateTimeProviderInterface` class - have been replaced with `Symfony\Component\Clock\ClockInterface` class. +* The `sylius/calendar` package has been replaced with `symfony/clock` package. All usages of + the `Sylius\Calendar\Provider\DateTimeProviderInterface` class + have been replaced with `Symfony\Component\Clock\ClockInterface` class. - * The following classes were changed: + * The following classes were changed: - * `Sylius\Bundle\CoreBundle\CatalogPromotion\Announcer\CatalogPromotionAnnouncer` - * `Sylius\Bundle\CoreBundle\MessageHandler\Admin\Account\RequestResetPasswordEmailHandler` - * `Sylius\Bundle\CoreBundle\PriceHistory\Logger\PriceChangeLogger` - * `Sylius\Bundle\CoreBundle\PriceHistory\Remover\ChannelPricingLogEntriesRemover` - * `Sylius\Bundle\ShippingBundle\Assigner\ShippingDateAssigner` - * `Sylius\Bundle\PromotionBundle\Criteria\DateRange` - * `Sylius\Bundle\ApiBundle\Applicator\ArchivingShippingMethodApplicator` - * `Sylius\Bundle\ApiBundle\CommandHandler\Account\RequestResetPasswordTokenHandler` - * `Sylius\Bundle\ApiBundle\CommandHandler\Account\VerifyCustomerAccountHandler` - * `Sylius\Component\Taxation\Checker\TaxRateDateEligibilityChecker` + * `Sylius\Bundle\CoreBundle\CatalogPromotion\Announcer\CatalogPromotionAnnouncer` + * `Sylius\Bundle\CoreBundle\MessageHandler\Admin\Account\RequestResetPasswordEmailHandler` + * `Sylius\Bundle\CoreBundle\PriceHistory\Logger\PriceChangeLogger` + * `Sylius\Bundle\CoreBundle\PriceHistory\Remover\ChannelPricingLogEntriesRemover` + * `Sylius\Bundle\ShippingBundle\Assigner\ShippingDateAssigner` + * `Sylius\Bundle\PromotionBundle\Criteria\DateRange` + * `Sylius\Bundle\ApiBundle\Applicator\ArchivingShippingMethodApplicator` + * `Sylius\Bundle\ApiBundle\CommandHandler\Account\RequestResetPasswordTokenHandler` + * `Sylius\Bundle\ApiBundle\CommandHandler\Account\VerifyCustomerAccountHandler` + * `Sylius\Component\Taxation\Checker\TaxRateDateEligibilityChecker` * The parameter order of `Sylius\Bundle\CoreBundle\Form\Type\Checkout\AddressType::__construct` has been changed: + ```php public function __construct( + private readonly AddressComparatorInterface $addressComparator, @@ -73,7 +78,9 @@ * The `\Serializable` interface has been removed from the `Sylius\Component\User\Model\UserInterface`. -* The parameter order of the `Sylius\Component\Core\OrderProcessing\OrderPaymentProcessor::__construct` has been changed: +* The parameter order of the `Sylius\Component\Core\OrderProcessing\OrderPaymentProcessor::__construct` has been + changed: + ```php public function __construct( private OrderPaymentProviderInterface $orderPaymentProvider, @@ -112,7 +119,67 @@ ): void; ``` +* The `swiftmailer/swiftmailer` dependency has been removed. Use `symfony/mailer` instead. + +* The following repository classes and interfaces were added, if you have custom repositories, + you need to update them to extend the new ones: + + Addressing: + + * `Sylius\Bundle\AddressingBundle\Doctrine\ORM\AddressRepository` + extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository` + implements `Sylius\Component\Addressing\Repository\AddressRepositoryInterface` + * `Sylius\Bundle\AddressingBundle\Doctrine\ORM\CountryRepository` + extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository` + implements `Sylius\Component\Addressing\Repository\CountryRepositoryInterface` + * `Sylius\Bundle\AddressingBundle\Doctrine\ORM\ProvinceRepository` + extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository` + implements `Sylius\Component\Addressing\Repository\ProvinceRepositoryInterface` + * `Sylius\Bundle\AddressingBundle\Doctrine\ORM\ZoneMemberRepository` + extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository` + implements `Sylius\Component\Addressing\Repository\ZoneMemberRepositoryInterface` + + Attribute: + + * `Sylius\Bundle\AttributeBundle\Doctrine\ORM\AttributeRepository` + extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository` + implements `Sylius\Component\Attribute\Repository\AttributeRepositoryInterface` + * `Sylius\Bundle\AttributeBundle\Doctrine\ORM\AttributeTranslationRepository` + extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository` + implements `Sylius\Component\Attribute\Repository\AttributeTranslationRepositoryInterface` + * `Sylius\Bundle\AttributeBundle\Doctrine\ORM\AttributeValueRepository` + extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository` + implements `Sylius\Component\Attribute\Repository\AttributeValueRepositoryInterface` + + Currency: + + * `Sylius\Bundle\CurrencyBundle\Doctrine\ORM\CurrencyRepository` + extends `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository` + implements `Sylius\Component\Currency\Repository\CurrencyRepositoryInterface` + +* The following repository classes and interfaces namespaces were changed, if you have custom repositories, + you need to update them to extend the new ones: + + Addressing: + + * `Sylius\Bundle\CoreBundle\Doctrine\ORM\AddressRepository` extended class changed from + `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository` to + `Sylius\Bundle\AddressingBundle\Doctrine\ORM\AddressRepository` + * `Sylius\Component\Core\Repository\AddressRepositoryInterface` implemented interface changed from + `Sylius\Component\Resource\Repository\RepositoryInterface` to + `Sylius\Component\Addressing\Repository\AddressRepositoryInterface` + + Attribute: + + * `Sylius\Bundle\CoreBundle\Doctrine\ORM\AttributeRepository` extended class changed from + `Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository` to + `Sylius\Bundle\AttributeBundle\Doctrine\ORM\AttributeRepository` + * `Sylius\Component\Product\Repository\ProductAttributeValueRepositoryInterface` extended interface changed from + `Sylius\Component\Resource\Repository\RepositoryInterface` to + `Sylius\Component\Attribute\Repository\AttributeValueRepositoryInterface` + ## Frontend -* `use_webpack` option was removed from the `sylius_ui` configuration, and the Webpack has become the only module bundler provided by Sylius. +* `use_webpack` option was removed from the `sylius_ui` configuration, and the Webpack has become the only module + bundler provided by Sylius. * `use_webpack` twig global variable was removed. Webpack is always used now, and there is no need to check for it. diff --git a/composer-require-checker.json b/composer-require-checker.json index eb600991d2..2640a0c24c 100644 --- a/composer-require-checker.json +++ b/composer-require-checker.json @@ -3,7 +3,6 @@ "null", "true", "false", "static", "self", "parent", "array", "string", "int", "float", "bool", "iterable", "callable", "void", "object", "mixed", "never", - "Swift_Message", "ApiPlatform\\Core\\Api\\FilterInterface", "ApiPlatform\\Core\\Api\\IriConverterInterface", "ApiPlatform\\Core\\Api\\OperationType", @@ -82,11 +81,12 @@ "Payum\\Core\\Security\\TokenInterface", "Payum\\Core\\Security\\Util\\Random", "Payum\\Core\\Storage\\AbstractStorage", + "Payum\\Paypal\\ExpressCheckout\\Nvp\\PaypalExpressCheckoutGatewayFactory", + "Payum\\Stripe\\StripeCheckoutGatewayFactory", "PHPUnit\\Framework\\ExpectationFailedException", "Psr\\Container\\ContainerInterface", "Psr\\Http\\Message\\RequestFactoryInterface", "Psr\\Http\\Message\\StreamFactoryInterface", - "Stripe\\Stripe", "Symfony\\Component\\Security\\Core\\Encoder\\EncoderFactoryInterface" ], "php-core-extensions" : [ diff --git a/composer.json b/composer.json index 6e43c7c9a8..7ecc27933d 100644 --- a/composer.json +++ b/composer.json @@ -62,9 +62,7 @@ "liip/imagine-bundle": "^2.10", "nyholm/psr7": "^1.6", "pagerfanta/pagerfanta": "^3.0", - "payum/core": "^1.7.3", "payum/offline": "^1.7.3", - "payum/paypal-express-checkout-nvp": "^1.7.3", "payum/payum-bundle": "^2.5", "php-http/httplug": "^2.4", "php-http/message-factory": "^1.0", @@ -186,6 +184,7 @@ "sylius/user-bundle": "self.version" }, "conflict": { + "doctrine/doctrine-bundle": "2.11.0", "doctrine/orm": ">= 2.16.0", "stof/doctrine-extensions-bundle": "1.8.0" }, @@ -211,6 +210,8 @@ "matthiasnoback/symfony-dependency-injection-test": "^4.2", "mikey179/vfsstream": "^1.6", "mockery/mockery": "^1.4", + "payum/paypal-express-checkout-nvp": "^1.7.3", + "payum/stripe": "^1.7.3", "phparkitect/phparkitect": "^0.2.9", "phpspec/phpspec": "^7.2", "phpspec/prophecy-phpunit": "^2.0", @@ -223,7 +224,6 @@ "psalm/plugin-mockery": "1.1.0", "psr/event-dispatcher": "^1.0", "rector/rector": "^0.18.0", - "stripe/stripe-php": "^12.0", "sylius-labs/coding-standard": "^4.2", "symfony/browser-kit": "^6.3.2", "symfony/debug-bundle": "^6.3.2", diff --git a/disabledTests/Api/Admin/ChannelsTest.php b/disabledTests/Api/Admin/ChannelsTest.php index d9bad69d40..7591bea486 100644 --- a/disabledTests/Api/Admin/ChannelsTest.php +++ b/disabledTests/Api/Admin/ChannelsTest.php @@ -44,6 +44,61 @@ final class ChannelsTest extends JsonApiTestCase ); } + /** @test */ + public function it_deletes_a_channel(): void + { + $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml']); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + $this->client->request( + method: 'GET', + uri: '/api/v2/admin/channels/MOBILE', + server: $header, + ); + + $this->assertResponseCode($this->client->getResponse(), Response::HTTP_OK); + + $this->client->request( + method: 'DELETE', + uri: '/api/v2/admin/channels/MOBILE', + server: $header, + ); + + $this->assertResponseCode($this->client->getResponse(), Response::HTTP_NO_CONTENT); + + $this->client->request( + method: 'GET', + uri: '/api/v2/admin/channels/MOBILE', + server: $header, + ); + + $this->assertResponseCode($this->client->getResponse(), Response::HTTP_NOT_FOUND); + } + + /** @test */ + public function it_prevents_deleting_the_only_channel(): void + { + $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml']); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + $this->client->request( + method: 'DELETE', + uri: '/api/v2/admin/channels/MOBILE', + server: $header, + ); + $this->client->request( + method: 'DELETE', + uri: '/api/v2/admin/channels/WEB', + server: $header, + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/channel/delete_channel_that_cannot_be_deleted', + Response::HTTP_UNPROCESSABLE_ENTITY + ); + } + /** @test */ public function it_gets_channels(): void { @@ -119,7 +174,15 @@ final class ChannelsTest extends JsonApiTestCase uri: '/api/v2/admin/channels/' . $channel->getCode(), server: $header, content: json_encode([ + 'defaultLocale' => '/api/v2/admin/locales/en_US', + 'locales' => ['/api/v2/admin/locales/en_US'], 'shippingAddressInCheckoutRequired' => true, + 'taxCalculationStrategy' => 'order_items_based', + 'accountVerificationRequired' => false, + 'name' => 'Web Store', + 'description' => 'different description', + 'hostname' => 'updated-hostname.com', + 'color' => 'blue', ], JSON_THROW_ON_ERROR), ); @@ -129,4 +192,48 @@ final class ChannelsTest extends JsonApiTestCase Response::HTTP_OK, ); } + + /** @test */ + public function it_updates_a_shop_billing_data(): void + { + $fixtures = $this->loadFixturesFromFiles(['authentication/api_administrator.yaml', 'channel.yaml']); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var ChannelInterface $channel */ + $channel = $fixtures['channel_web']; + + $this->client->request( + method: 'PUT', + uri: sprintf('/api/v2/admin/channels/%s', $channel->getCode()), + server: $header, + content: json_encode([ + 'shopBillingData' => [ + '@id' => sprintf('/api/v2/admin/shop-billing-datas/%s', $channel->getShopBillingData()->getId()), + 'company' => 'DifferentCompany', + 'taxId' => '123', + 'countryCode' => 'DE', + 'street' => 'Different Street', + 'city' => 'different City', + 'postcode' => '12-124' + ] + ], JSON_THROW_ON_ERROR), + ); + + $this->assertResponseCode( + $this->client->getResponse(), + Response::HTTP_OK, + ); + + $this->client->request( + method: 'GET', + uri: sprintf('/api/v2/admin/channels/%s/shop-billing-data', $channel->getCode()), + server: $header, + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/put_shop_billing_data_response', + Response::HTTP_OK, + ); + } } diff --git a/disabledTests/Api/Admin/ProductAssociationTypesTest.php b/disabledTests/Api/Admin/ProductAssociationTypesTest.php index 6265563200..f43ac45af1 100644 --- a/disabledTests/Api/Admin/ProductAssociationTypesTest.php +++ b/disabledTests/Api/Admin/ProductAssociationTypesTest.php @@ -38,7 +38,7 @@ final class ProductAssociationTypesTest extends JsonApiTestCase $this->assertResponse( $this->client->getResponse(), - 'admin/get_product_association_type_response', + 'admin/product_association_type/get_product_association_type_response', Response::HTTP_OK, ); } @@ -70,7 +70,7 @@ final class ProductAssociationTypesTest extends JsonApiTestCase $this->assertResponse( $this->client->getResponse(), - 'admin/get_product_association_type_collection_response', + 'admin/product_association_type/get_product_association_type_collection_response', Response::HTTP_OK, ); } @@ -97,11 +97,31 @@ final class ProductAssociationTypesTest extends JsonApiTestCase $this->assertResponse( $this->client->getResponse(), - 'admin/post_product_association_type_response', + 'admin/product_association_type/post_product_association_type_response', Response::HTTP_CREATED, ); } + /** @test */ + public function it_does_not_create_product_association_type_without_required_data(): void + { + $this->loadFixturesFromFiles(['product/product_with_many_locales.yaml', 'authentication/api_administrator.yaml']); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + $this->client->request( + method: 'POST', + uri: '/api/v2/admin/product-association-types', + server: $header, + content: '{}', + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/product_association_type/post_product_association_type_without_required_data_response', + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } + /** @test */ public function it_updates_product_association_type(): void { @@ -116,17 +136,22 @@ final class ProductAssociationTypesTest extends JsonApiTestCase server: $header, content: json_encode([ 'code' => 'TEST', - 'translations' => ['en_US' => [ - 'name' => 'test', - 'description' => 'test description', - 'locale' => 'de_DE' - ]] + 'translations' => [ + 'en_US' => [ + '@id' => sprintf('/api/v2/admin/product-association-type-translations/%s', $associationType->getTranslation('en_US')->getId()), + 'name' => 'Similar products', + ], + 'de_DE' => [ + 'name' => 'test', + 'description' => 'test description' + ], + ] ], JSON_THROW_ON_ERROR), ); $this->assertResponse( $this->client->getResponse(), - 'admin/put_product_association_type_response', + 'admin/product_association_type/put_product_association_type_response', Response::HTTP_OK, ); } diff --git a/disabledTests/Api/Admin/ProductAttributesTest.php b/disabledTests/Api/Admin/ProductAttributesTest.php index 5dd1cfdd90..640d9d7319 100644 --- a/disabledTests/Api/Admin/ProductAttributesTest.php +++ b/disabledTests/Api/Admin/ProductAttributesTest.php @@ -394,13 +394,7 @@ final class ProductAttributesTest extends JsonApiTestCase method: 'POST', uri: '/api/v2/admin/product-attributes', server: $header, - content: json_encode([ - 'translations' => [ - 'en_US' => [ - 'locale' => 'en_US', - ], - ], - ], JSON_THROW_ON_ERROR), + content: '{}', ); $this->assertResponse( @@ -423,6 +417,12 @@ final class ProductAttributesTest extends JsonApiTestCase content: json_encode([ 'code' => 'test', 'type' => 'foobar', + 'translations' => [ + 'en_US' => [ + 'locale' => 'en_US', + 'name' => 'Test', + ], + ], ], JSON_THROW_ON_ERROR), ); diff --git a/disabledTests/Api/Admin/ProductVariantsTest.php b/disabledTests/Api/Admin/ProductVariantsTest.php index beacad759d..c070703e67 100644 --- a/disabledTests/Api/Admin/ProductVariantsTest.php +++ b/disabledTests/Api/Admin/ProductVariantsTest.php @@ -141,7 +141,7 @@ final class ProductVariantsTest extends JsonApiTestCase } /** @test */ - public function it_creates_a_product_variant_enabled_by_default(): void + public function it_creates_a_product_variant_enabled_by_default_with_translation_in_default_locale(): void { $this->loadFixturesFromFiles([ 'authentication/api_administrator.yaml', @@ -168,7 +168,7 @@ final class ProductVariantsTest extends JsonApiTestCase $this->assertResponse( $this->client->getResponse(), - 'admin/product_variant/post_product_variant_enabled_by_default_response', + 'admin/product_variant/post_product_variant_enabled_by_default_with_translation_in_default_locale_response', Response::HTTP_CREATED, ); } @@ -325,13 +325,11 @@ final class ProductVariantsTest extends JsonApiTestCase 'minimumPrice' => 500, ]], 'translations' => [ - 'pl_PL' => [ - '@id' => sprintf('/api/v2/admin/product-variant-translations/%s', $productVariant->getTranslation('pl_PL')->getId()), - 'locale' => 'pl_PL', - 'name' => 'Czerwony kubek', + 'en_US' => [ + '@id' => sprintf('/api/v2/admin/product-variant-translations/%s', $productVariant->getTranslation('en_US')->getId()), + 'name' => 'Red mug', ], 'de_DE' => [ - 'locale' => 'de_DE', 'name' => 'Rote Tasse', ], ], @@ -396,6 +394,41 @@ final class ProductVariantsTest extends JsonApiTestCase $this->assertResponseCode($this->client->getResponse(), Response::HTTP_UNPROCESSABLE_ENTITY); } + /** @test */ + public function it_does_not_allow_to_update_product_variant_without_translation_in_default_locale(): void + { + $fixtures = $this->loadFixturesFromFiles([ + 'authentication/api_administrator.yaml', + 'channel.yaml', + 'tax_category.yaml', + 'shipping_category.yaml', + 'product/product_variant.yaml', + ]); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + /** @var ProductVariantInterface $productVariant */ + $productVariant = $fixtures['product_variant']; + + $this->client->request( + method: 'PUT', + uri: sprintf('/api/v2/admin/product-variants/%s', $productVariant->getCode()), + server: $header, + content: json_encode([ + 'translations' => [ + 'de_DE' => [ + 'name' => 'Tasse', + ], + ], + ], JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/product_variant/put_product_variant_without_translation_in_default_locale_response', + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } + /** @test */ public function it_deletes_the_product_variant(): void { diff --git a/disabledTests/Api/Admin/ProductsTest.php b/disabledTests/Api/Admin/ProductsTest.php index 4e8305b1a3..bd47f2610f 100644 --- a/disabledTests/Api/Admin/ProductsTest.php +++ b/disabledTests/Api/Admin/ProductsTest.php @@ -99,7 +99,6 @@ final class ProductsTest extends JsonApiTestCase ]], 'translations' => [ 'en_US' => [ - 'locale' => 'en_US', 'slug' => 'mug', 'name' => 'Mug', 'description' => 'This is a mug', @@ -108,7 +107,6 @@ final class ProductsTest extends JsonApiTestCase 'metaDescription' => 'Mug description', ], 'pl_PL' => [ - 'locale' => 'pl_PL', 'slug' => 'kubek', 'name' => 'Kubek', 'description' => 'To jest kubek', @@ -127,6 +125,58 @@ final class ProductsTest extends JsonApiTestCase ); } + /** @test */ + public function it_does_not_create_a_product_without_required_data(): void + { + $this->loadFixturesFromFile('authentication/api_administrator.yaml'); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + $this->client->request( + method: 'POST', + uri: '/api/v2/admin/products', + server: $header, + content: '{}', + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/product/post_product_without_required_data_response', + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } + + /** @test */ + public function it_does_not_create_a_product_with_invalid_translation_locale(): void + { + $this->loadFixturesFromFile('authentication/api_administrator.yaml'); + $header = array_merge($this->logInAdminUser('api@example.com'), self::CONTENT_TYPE_HEADER); + + $this->client->request( + method: 'POST', + uri: '/api/v2/admin/products', + server: $header, + content: json_encode([ + 'code' => 'MUG', + 'translations' => [ + 'en_US' => [ + 'slug' => 'mug', + 'name' => 'Mug', + ], + 'a' => [ + 'slug' => 'kubek', + 'name' => 'Kubek', + ], + ], + ], JSON_THROW_ON_ERROR), + ); + + $this->assertResponse( + $this->client->getResponse(), + 'admin/product/post_product_with_invalid_translation_locale', + Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } + /** @test */ public function it_updates_the_existing_product(): void { @@ -178,7 +228,6 @@ final class ProductsTest extends JsonApiTestCase 'translations' => [ 'en_US' => [ '@id' => sprintf('/api/v2/admin/product-translations/%s', $product->getTranslation('en_US')->getId()), - 'locale' => 'en_US', 'slug' => 'caps/cap', 'name' => 'Cap', 'description' => 'This is a cap', @@ -188,7 +237,6 @@ final class ProductsTest extends JsonApiTestCase ], 'pl_PL' => [ '@id' => sprintf('/api/v2/admin/product-translations/%s', $product->getTranslation('pl_PL')->getId()), - 'locale' => 'pl_PL', 'slug' => 'czapki/czapka', 'name' => 'Czapka', 'description' => 'To jest czapka', diff --git a/disabledTests/Api/Admin/TaxonsTest.php b/disabledTests/Api/Admin/TaxonsTest.php index efc49842cd..fb5bb0c57c 100644 --- a/disabledTests/Api/Admin/TaxonsTest.php +++ b/disabledTests/Api/Admin/TaxonsTest.php @@ -99,13 +99,7 @@ final class TaxonsTest extends JsonApiTestCase method: 'POST', uri: '/api/v2/admin/taxons', server: $header, - content: json_encode([ - 'translations' => [ - 'en_US' => [ - 'locale' => 'en_US', - ] - ] - ], JSON_THROW_ON_ERROR), + content: '{}', ); $this->assertResponse( diff --git a/features/channel/managing_channels/adding_channel.feature b/features/channel/managing_channels/adding_channel.feature index aa48daf64c..32265b9a97 100644 --- a/features/channel/managing_channels/adding_channel.feature +++ b/features/channel/managing_channels/adding_channel.feature @@ -10,7 +10,7 @@ Feature: Adding a new channel And the store operates in "United States" and "Poland" And I am logged in as an administrator - @ui @api + @api @ui Scenario: Adding a new channel When I want to create a new channel And I specify its code as "MOBILE" @@ -23,7 +23,7 @@ Feature: Adding a new channel Then I should be notified that it has been successfully created And the channel "Mobile channel" should appear in the registry - @ui @api + @api @ui Scenario: Adding a new channel with additional fields When I want to create a new channel And I specify its code as "MOBILE" diff --git a/features/channel/managing_channels/adding_channel_with_shop_billing_data.feature b/features/channel/managing_channels/adding_channel_with_shop_billing_data.feature index dd3e351a7f..cb405a8382 100644 --- a/features/channel/managing_channels/adding_channel_with_shop_billing_data.feature +++ b/features/channel/managing_channels/adding_channel_with_shop_billing_data.feature @@ -10,7 +10,7 @@ Feature: Adding a new channel with shop billing data And the store operates in "United States" And I am logged in as an administrator - @ui @api + @api @ui Scenario: Adding a new channel with shop billing data When I want to create a new channel And I specify its code as "MOBILE" diff --git a/features/channel/managing_channels/browsing_channels.feature b/features/channel/managing_channels/browsing_channels.feature index f95b116ae0..070993551b 100644 --- a/features/channel/managing_channels/browsing_channels.feature +++ b/features/channel/managing_channels/browsing_channels.feature @@ -9,7 +9,7 @@ Feature: Browsing channels And the store operates on another channel named "Mobile Channel" And I am logged in as an administrator - @ui @api + @api @ui Scenario: Browsing defined channels When I want to browse channels Then I should see 2 channels in the list diff --git a/features/channel/managing_channels/channel_unique_code_validation.feature b/features/channel/managing_channels/channel_unique_code_validation.feature index f8b6e62cff..47d6543623 100644 --- a/features/channel/managing_channels/channel_unique_code_validation.feature +++ b/features/channel/managing_channels/channel_unique_code_validation.feature @@ -8,7 +8,7 @@ Feature: Channel unique code validation Given the store operates on a channel identified by "WEB" code And I am logged in as an administrator - @ui + @api @ui Scenario: Trying to add channel with taken code When I want to create a new channel And I specify its code as "WEB" diff --git a/features/channel/managing_channels/channel_validation.feature b/features/channel/managing_channels/channel_validation.feature index fe45e18b4b..766a3a4a2c 100644 --- a/features/channel/managing_channels/channel_validation.feature +++ b/features/channel/managing_channels/channel_validation.feature @@ -7,7 +7,7 @@ Feature: Channel validation Background: Given I am logged in as an administrator - @ui + @api @ui Scenario: Trying to add a new channel without specifying its code When I want to create a new channel And I name it "Mobile channel" @@ -16,7 +16,7 @@ Feature: Channel validation Then I should be notified that code is required And channel with name "Mobile channel" should not be added - @ui + @api @ui Scenario: Trying to add a new channel without specifying its name When I want to create a new channel And I specify its code as "MOBILE" @@ -25,7 +25,7 @@ Feature: Channel validation Then I should be notified that name is required And channel with code "MOBILE" should not be added - @ui + @api @ui Scenario: Trying to add a new channel without base currency When I want to create a new channel And I specify its code as "MOBILE" @@ -34,7 +34,7 @@ Feature: Channel validation Then I should be notified that base currency is required And channel with code "MOBILE" should not be added - @ui + @api @ui Scenario: Trying to add a new channel without default locale When I want to create a new channel And I specify its code as "MOBILE" @@ -43,7 +43,7 @@ Feature: Channel validation Then I should be notified that default locale is required And channel with code "MOBILE" should not be added - @ui + @api @ui Scenario: Trying to remove name from existing channel Given the store operates on a channel named "Web Channel" When I want to modify this channel diff --git a/features/channel/managing_channels/deleting_channel.feature b/features/channel/managing_channels/deleting_channel.feature index 372e70c0eb..fb2976984b 100644 --- a/features/channel/managing_channels/deleting_channel.feature +++ b/features/channel/managing_channels/deleting_channel.feature @@ -9,7 +9,7 @@ Feature: Deleting a channel And the store operates on another channel named "Mobile Store" And I am logged in as an administrator - @ui + @api @ui Scenario: Deleted channel should disappear from the registry When I delete channel "Web Store" Then I should be notified that it has been successfully deleted diff --git a/features/channel/managing_channels/editing_channel.feature b/features/channel/managing_channels/editing_channel.feature index 4a011b1f75..42e7fa0046 100644 --- a/features/channel/managing_channels/editing_channel.feature +++ b/features/channel/managing_channels/editing_channel.feature @@ -8,20 +8,12 @@ Feature: Editing channel Given the store operates on a channel named "Web Channel" And I am logged in as an administrator - @todo - Scenario: Trying to change channel code + @api @ui + Scenario: Being unable to change the code of an existing channel When I want to modify a channel "Web Channel" - And I change its code to "MOBILE" - And I save my changes - Then I should be notified that code cannot be changed - And channel "Web Channel" should still have code "MOBILE" + Then I should not be able to edit its code - @ui - Scenario: Seeing disabled code field when editing channel - When I want to modify a channel "Web Channel" - Then the code field should be disabled - - @ui + @api @ui Scenario: Renaming the channel When I want to modify a channel "Web Channel" And I rename it to "Website store" @@ -29,7 +21,7 @@ Feature: Editing channel Then I should be notified that it has been successfully edited And this channel name should be "Website store" - @ui - Scenario: Seeing disabled base currency field during channel edition + @api @ui + Scenario: Being unable to change base currency of an existing channel When I want to modify a channel "Web Channel" - Then the base currency field should be disabled + Then I should not be able to edit its base currency diff --git a/features/channel/managing_channels/editing_menu_taxon_on_channel.feature b/features/channel/managing_channels/editing_menu_taxon_on_channel.feature index bb32cbf8b5..e0440d57a1 100644 --- a/features/channel/managing_channels/editing_menu_taxon_on_channel.feature +++ b/features/channel/managing_channels/editing_menu_taxon_on_channel.feature @@ -11,7 +11,7 @@ Feature: Editing menu taxon on channel And channel "Web Store" has menu taxon "Clothes" And I am logged in as an administrator - @ui @javascript + @api @ui @javascript Scenario: Editing menu taxon on the channel When I want to modify a channel "Web Store" And I change its menu taxon to "Guns" diff --git a/features/channel/managing_channels/editing_shop_billing_data_on_channel.feature b/features/channel/managing_channels/editing_shop_billing_data_on_channel.feature index 801ae49a6e..d1bb24601a 100644 --- a/features/channel/managing_channels/editing_shop_billing_data_on_channel.feature +++ b/features/channel/managing_channels/editing_shop_billing_data_on_channel.feature @@ -10,14 +10,20 @@ Feature: Editing shop billing data on channel And channel "Web Store" billing data is "Ragnarok", "Pacific Coast Hwy", "90806" "Los Angeles", "United States" with "1100110011" tax ID And I am logged in as an administrator - @ui + @api @ui Scenario: Editing shop billing data on channel When I want to modify a channel "Web Store" - And I specify company as "Götterdämmerung" - And I specify tax ID as "666777" - And I specify shop billing address as "Valhalla", "123" "Asgard", "United States" + And I specify shop billing data for this channel as "Götterdämmerung", "Valhalla", "123", "Asgard", "666777" tax ID and "United States" country And I save my changes Then I should be notified that it has been successfully edited And this channel company should be "Götterdämmerung" And this channel tax ID should be "666777" - And this channel shop billing address should be "Valhalla", "123" "Asgard", "United States" + And this channel shop billing address should be "Valhalla", "123" "Asgard" and "United States" country + + @api @no-ui + Scenario: Editing shop billing data with wrong country code + When I want to modify a channel "Web Store" + And I specify new country code for this channel as "ZZ" + And I save my changes + Then I should be notified that it is not a valid country + And this channel shop billing address should still be "Pacific Coast Hwy", "90806" "Los Angeles" and "United States" country diff --git a/features/channel/managing_channels/not_being_able_to_add_disabled_channel_when_no_other_exist.feature b/features/channel/managing_channels/not_being_able_to_add_disabled_channel_when_no_other_exist.feature index c9d19fd580..31f2cb450e 100644 --- a/features/channel/managing_channels/not_being_able_to_add_disabled_channel_when_no_other_exist.feature +++ b/features/channel/managing_channels/not_being_able_to_add_disabled_channel_when_no_other_exist.feature @@ -9,8 +9,8 @@ Feature: Not being able to add a disabled channel when no other exist And the store has locale "English (United States)" And I am logged in as an administrator - @ui - Scenario: Adding a new disabled channel should result + @api @ui + Scenario: Trying to add a new disabled channel when no other exist When I want to create a new channel And I specify its code as "MOBILE" And I name it "Mobile channel" diff --git a/features/channel/managing_channels/not_being_able_to_delete_last_available_channel.feature b/features/channel/managing_channels/not_being_able_to_delete_last_available_channel.feature index e30a65d27c..f46c66cb40 100644 --- a/features/channel/managing_channels/not_being_able_to_delete_last_available_channel.feature +++ b/features/channel/managing_channels/not_being_able_to_delete_last_available_channel.feature @@ -8,8 +8,8 @@ Feature: Not being able to delete a last available channel Given the store operates on a channel named "Web Store" And I am logged in as an administrator - @ui - Scenario: Prevented from deleting only channel + @api @ui + Scenario: Preventing from deleting only channel When I delete channel "Web Store" Then I should be notified that it cannot be deleted And this channel should still be in the registry diff --git a/features/channel/managing_channels/not_being_able_to_disable_last_available_channel.feature b/features/channel/managing_channels/not_being_able_to_disable_last_available_channel.feature index bb95ad1290..b7e302c4b4 100644 --- a/features/channel/managing_channels/not_being_able_to_disable_last_available_channel.feature +++ b/features/channel/managing_channels/not_being_able_to_disable_last_available_channel.feature @@ -8,7 +8,7 @@ Feature: Toggling a channel Given the store operates on a channel named "Web Channel" And I am logged in as an administrator - @ui + @api @ui Scenario: Disabling the last available channel Given the channel "Web Channel" is enabled When I want to modify this channel diff --git a/features/channel/managing_channels/selecting_currencies_available_for_channel.feature b/features/channel/managing_channels/selecting_currencies_available_for_channel.feature index b478e47268..339b63a728 100644 --- a/features/channel/managing_channels/selecting_currencies_available_for_channel.feature +++ b/features/channel/managing_channels/selecting_currencies_available_for_channel.feature @@ -7,20 +7,24 @@ Feature: Selecting available currencies for a channel Background: Given the store has currency "Euro" And the store has locale "English (United States)" + And the store operates in "United States" And I am logged in as an administrator - @ui + @api @ui Scenario: Adding a new channel with currencies When I want to create a new channel And I specify its code as MOBILE + And I choose "Euro" as the base currency And I name it "Mobile store" And I allow for paying in "Euro" + And I make it available in "English (United States)" And I choose "English (United States)" as a default locale + And I select the "Order items based" as tax calculation strategy And I add it Then I should be notified that it has been successfully created And paying in Euro should be possible for the "Mobile store" channel - @ui + @api @ui Scenario: Adding currencies to an existing channel Given the store operates on a channel named "Web store" When I want to modify this channel diff --git a/features/channel/managing_channels/selecting_default_tax_zone_for_channel.feature b/features/channel/managing_channels/selecting_default_tax_zone_for_channel.feature index 891dcd70ae..bc3e2ef5db 100644 --- a/features/channel/managing_channels/selecting_default_tax_zone_for_channel.feature +++ b/features/channel/managing_channels/selecting_default_tax_zone_for_channel.feature @@ -8,19 +8,21 @@ Feature: Selecting default tax zone for a channel Given the store operates on a single channel in "United States" And I am logged in as an administrator - @ui + @api @ui Scenario: Adding a new channel with default tax zone When I want to create a new channel And I specify its code as "MOBILE" And I name it "Mobile store" And I select the "United States" as default tax zone And I choose "USD" as the base currency + And I make it available in "English (United States)" And I choose "English (United States)" as a default locale + And I select the "Order items based" as tax calculation strategy And I add it Then I should be notified that it has been successfully created And the default tax zone for the "Mobile store" channel should be "United States" - @ui + @api @ui Scenario: Selecting default tax zone for existing channel Given the store operates on a channel named "Web store" When I want to modify this channel @@ -29,7 +31,7 @@ Feature: Selecting default tax zone for a channel Then I should be notified that it has been successfully edited And the default tax zone for the "Web store" channel should be "United States" - @ui + @api @ui Scenario: Removing existing channel default tax zone Given the store operates on a channel named "Web store" And its default tax zone is zone "US" diff --git a/features/channel/managing_channels/selecting_locales_available_for_channel.feature b/features/channel/managing_channels/selecting_locales_available_for_channel.feature index 25afdd3ff9..1c17675e3a 100644 --- a/features/channel/managing_channels/selecting_locales_available_for_channel.feature +++ b/features/channel/managing_channels/selecting_locales_available_for_channel.feature @@ -9,7 +9,7 @@ Feature: Selecting available locales for a channel And the store has locale "English (United States)" And I am logged in as an administrator - @ui + @api @ui Scenario: Adding a new channel with locales When I want to create a new channel And I specify its code as "MOBILE" @@ -17,11 +17,12 @@ Feature: Selecting available locales for a channel And I make it available in "English (United States)" And I choose "Euro" as the base currency And I choose "English (United States)" as a default locale + And I select the "Order items based" as tax calculation strategy And I add it Then I should be notified that it has been successfully created And the channel "Mobile Channel" should be available in "English (United States)" - @ui + @api @ui Scenario: Adding locales to an existing channel Given the store operates on a channel named "Web Channel" When I want to modify this channel @@ -30,23 +31,23 @@ Feature: Selecting available locales for a channel Then I should be notified that it has been successfully edited And the channel "Web Channel" should be available in "English (United States)" - @ui + @api @ui Scenario: Being unable to disable locale used as the default one for a channel Given the store operates on a channel named "Web" And this channel allows to shop using "English (United States)" and "Polish (Poland)" locales And this channel uses the "English (United States)" locale as default - And I am modifying a channel "Web" - When I make it available only in "Polish (Poland)" + When I want to modify this channel + And I make it available only in "Polish (Poland)" And I try to save my changes Then I should be notified that the default locale has to be enabled - @ui + @api @ui Scenario: Being unable to set disabled locale as a default one for a channel Given the store has locale "Polish (Poland)" And the store operates on a channel named "Web" And this channel allows to shop using the "English (United States)" locale And this channel uses the "English (United States)" locale as default - And I am modifying a channel "Web" - When I choose "Polish (Poland)" as a default locale + When I want to modify this channel + And I choose "Polish (Poland)" as a default locale And I try to save my changes Then I should be notified that the default locale has to be enabled diff --git a/features/channel/managing_channels/selecting_tax_calculation_strategy_for_channel.feature b/features/channel/managing_channels/selecting_tax_calculation_strategy_for_channel.feature index ff8f6bac6f..8b2d560df7 100644 --- a/features/channel/managing_channels/selecting_tax_calculation_strategy_for_channel.feature +++ b/features/channel/managing_channels/selecting_tax_calculation_strategy_for_channel.feature @@ -20,19 +20,20 @@ Feature: Selecting tax calculation strategy for a channel Then I should be notified that it has been successfully created And the tax calculation strategy for the "Mobile store" channel should be "Order items based" - @ui + @api @ui Scenario: Adding a new channel with tax calculation strategy When I want to create a new channel And I specify its code as "MOBILE" And I select the "Order item units based" as tax calculation strategy And I name it "Mobile store" And I choose "Euro" as the base currency + And I make it available in "English (United States)" And I choose "English (United States)" as a default locale And I add it Then I should be notified that it has been successfully created And the tax calculation strategy for the "Mobile store" channel should be "Order item units based" - @ui + @api @ui Scenario: Changing tax calculation strategy of existing channel Given the store operates on a channel named "Web store" When I want to modify this channel diff --git a/features/channel/managing_channels/toggling_channel.feature b/features/channel/managing_channels/toggling_channel.feature index 41875817f1..92d88bc4d0 100644 --- a/features/channel/managing_channels/toggling_channel.feature +++ b/features/channel/managing_channels/toggling_channel.feature @@ -9,7 +9,7 @@ Feature: Toggling a channel And the store operates on another channel named "Mobile Channel" And I am logged in as an administrator - @ui + @api @ui Scenario: Disabling the channel Given the channel "Web Channel" is enabled When I want to modify this channel @@ -18,7 +18,7 @@ Feature: Toggling a channel Then I should be notified that it has been successfully edited And this channel should be disabled - @ui + @api @ui Scenario: Enabling the channel Given the channel "Web Channel" is disabled When I want to modify this channel diff --git a/features/product/managing_product_attributes/seeing_correct_select_attribute_values_in_different_locale_than_default_one.feature b/features/product/managing_product_attributes/seeing_correct_select_attribute_values_in_different_locale_than_default_one.feature index 8e0eee07a3..9362431da5 100644 --- a/features/product/managing_product_attributes/seeing_correct_select_attribute_values_in_different_locale_than_default_one.feature +++ b/features/product/managing_product_attributes/seeing_correct_select_attribute_values_in_different_locale_than_default_one.feature @@ -8,7 +8,7 @@ Feature: Seeing correct select attribute values in different locale than default Given the store is available in "French (France)" And I am logged in as an administrator - @ui @javascript @api + @ui @javascript @no-api Scenario: Seeing correct attribute values in different locale than default one When I want to create a new select product attribute And I specify its code as "mug_material" diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8823b12446..41bfed5818 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1820,6 +1820,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/Serializer/ShippingMethodNormalizer.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\TranslatableDenormalizer\\:\\:hasDefaultTranslation\\(\\) has parameter \\$translations with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Serializer/TranslatableDenormalizer.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\ZoneDenormalizer\\:\\:getZoneMemberByCode\\(\\) has parameter \\$zoneMembers with generic interface Doctrine\\\\Common\\\\Collections\\\\Selectable but does not specify its types\\: TKey, T$#" count: 1 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f7c4b7da20..9390adf949 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -24,5 +24,7 @@ + + diff --git a/src/Sylius/Behat/Client/ApiClientInterface.php b/src/Sylius/Behat/Client/ApiClientInterface.php index 318fc077c9..7ae5bf4fbe 100644 --- a/src/Sylius/Behat/Client/ApiClientInterface.php +++ b/src/Sylius/Behat/Client/ApiClientInterface.php @@ -62,7 +62,9 @@ interface ApiClientInterface public function addFile(string $key, UploadedFile $file): void; - public function addRequestData(string $key, string|int|bool|array $value): void; + public function addRequestData(string $key, null|string|int|bool|array $value): void; + + public function replaceRequestData(string $key, null|string|int|bool|array $value): void; public function setSubResourceData(string $key, array $data): void; diff --git a/src/Sylius/Behat/Client/ApiPlatformClient.php b/src/Sylius/Behat/Client/ApiPlatformClient.php index d6b116cb74..efac250742 100644 --- a/src/Sylius/Behat/Client/ApiPlatformClient.php +++ b/src/Sylius/Behat/Client/ApiPlatformClient.php @@ -200,12 +200,18 @@ final class ApiPlatformClient implements ApiClientInterface $this->request->updateFiles([$key => $file]); } - /** @param string|int|bool|array $value */ - public function addRequestData(string $key, $value): void + public function addRequestData(string $key, null|string|int|bool|array $value): void { $this->request->updateContent([$key => $value]); } + public function replaceRequestData(string $key, null|string|int|bool|array $value): void + { + $requestContent = $this->request->getContent(); + + $this->request->setContent(array_replace($requestContent, [$key => $value])); + } + public function updateRequestData(array $data): void { $this->request->updateContent($data); diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsBillingDataContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsBillingDataContext.php new file mode 100644 index 0000000000..02ff55114c --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsBillingDataContext.php @@ -0,0 +1,83 @@ +getShopBillingDataFromChannel($channel); + + Assert::same($shopBillingData->getCompany(), $company); + } + + /** + * @Then /^(this channel) tax ID should be "([^"]+)"$/ + */ + public function thisChanneTaxIdShouldBe(ChannelInterface $channel, string $taxId): void + { + $shopBillingData = $this->getShopBillingDataFromChannel($channel); + + Assert::same($shopBillingData->getTaxId(), $taxId); + } + + /** + * @Then /^(this channel) shop billing address should be "([^"]+)", "([^"]+)" "([^"]+)" and ("([^"]+)" country)$/ + * @Then /^(this channel) shop billing address should still be "([^"]+)", "([^"]+)" "([^"]+)" and ("([^"]+)" country)$/ + */ + public function thisChannelShopBillingAddressShouldBe( + ChannelInterface $channel, + string $street, + string $postcode, + string $city, + CountryInterface $country, + ): void { + $shopBillingData = $this->getShopBillingDataFromChannel($channel); + + Assert::same($shopBillingData->getStreet(), $street); + Assert::same($shopBillingData->getPostcode(), $postcode); + Assert::same($shopBillingData->getCity(), $city); + Assert::same($shopBillingData->getCountryCode(), $country->getCode()); + } + + private function getShopBillingDataFromChannel(ChannelInterface $channel): ShopBillingDataInterface + { + $this->client->show(Resources::CHANNELS, $channel->getCode()); + + /** @var ShopBillingDataInterface $shopBillingData */ + $shopBillingData = $this->iriConverter->getResourceFromIri($this->responseChecker->getValue($this->client->getLastResponse(), 'shopBillingData')); + + return $shopBillingData; + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsContext.php index b1dee20e63..8546c610cf 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsContext.php @@ -20,6 +20,7 @@ use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface; use Sylius\Component\Addressing\Model\CountryInterface; +use Sylius\Component\Addressing\Model\ZoneInterface; use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\TaxonInterface; @@ -47,6 +48,14 @@ final class ManagingChannelsContext implements Context $this->client->buildCreateRequest(Resources::CHANNELS); } + /** + * @When I delete channel :channel + */ + public function iDeleteChannel(ChannelInterface $channel): void + { + $this->client->delete(Resources::CHANNELS, $channel->getCode()); + } + /** * @When I want to modify a channel :channel */ @@ -55,23 +64,78 @@ final class ManagingChannelsContext implements Context $this->client->buildUpdateRequest(Resources::CHANNELS, $channel->getCode()); } + /** + * @When I rename it to :name + * @When I do not name it + * @When I remove its name + */ + public function iRenameIt(string $name = ''): void + { + $this->client->addRequestData('name', $name); + } + + /** + * @When /^I (enable|disable) it$/ + */ + public function iDisableIt(string $toggleAction): void + { + $this->client->addRequestData('enabled', $toggleAction === 'enable'); + } + + /** + * @When I change its menu taxon to :taxon + */ + public function iChangeItsMenuTaxonTo(TaxonInterface $taxon): void + { + $this->client->addRequestData('menuTaxon', $this->sectionAwareIriConverter->getIriFromResourceInSection($taxon, 'admin')); + } + /** * @When I specify its :field as :value * @When I :field it :value * @When I set its :field as :value * @When I define its :field as :value + * @When I do not specify its :field */ - public function iSpecifyItsAs(string $field, string $value): void + public function iSpecifyItsAs(string $field, string $value = ''): void { $this->client->addRequestData($field, $value); } /** * @When I choose :currency as the base currency + * @When I do not choose base currency */ - public function iChooseAsTheBaseCurrency(CurrencyInterface $currency): void + public function iChooseAsTheBaseCurrency(?CurrencyInterface $currency = null): void { - $this->client->addRequestData('baseCurrency', $this->sectionAwareIriConverter->getIriFromResourceInSection($currency, 'admin')); + $this->client->addRequestData( + 'baseCurrency', + null === $currency ? $currency : $this->sectionAwareIriConverter->getIriFromResourceInSection($currency, 'admin'), + ); + } + + /** + * @When I allow for paying in :currency + */ + public function iAllowToPayingForThisChannel(CurrencyInterface $currency): void + { + $this->client->addRequestData('currencies', [$this->iriConverter->getIriFromResource($currency)]); + } + + /** + * @When I select the :zone as default tax zone + */ + public function iSelectDefaultTaxZone(ZoneInterface $zone): void + { + $this->client->addRequestData('defaultTaxZone', $this->iriConverter->getIriFromResource($zone)); + } + + /** + * @When I remove its default tax zone + */ + public function iRemoveItsDefaultTaxZone(): void + { + $this->client->addRequestData('defaultTaxZone', null); } /** @@ -79,15 +143,27 @@ final class ManagingChannelsContext implements Context */ public function iMakeItAvailableInLocale(LocaleInterface $locale): void { - $this->client->addRequestData('locales', [$this->iriConverter->getIriFromResource($locale)]); + $this->client->addRequestData('locales', [$this->sectionAwareIriConverter->getIriFromResourceInSection($locale, 'admin')]); + } + + /** + * @When I make it available only in :locale + */ + public function iMakeItAvailableOnlyInLocale(LocaleInterface $locale): void + { + $this->client->replaceRequestData('locales', [$this->sectionAwareIriConverter->getIriFromResourceInSection($locale, 'admin')]); } /** * @When I choose :locale as a default locale + * @When I do not choose default locale */ - public function iChooseAsADefaultLocale(LocaleInterface $locale): void + public function iChooseAsADefaultLocale(?LocaleInterface $locale = null): void { - $this->client->addRequestData('defaultLocale', $this->sectionAwareIriConverter->getIriFromResourceInSection($locale, 'admin')); + $this->client->addRequestData( + 'defaultLocale', + null === $locale ? $locale : $this->sectionAwareIriConverter->getIriFromResourceInSection($locale, 'admin'), + ); } /** @@ -165,6 +241,44 @@ final class ManagingChannelsContext implements Context $this->shopBillingData['taxId'] = $taxId; } + /** + * @When /^I specify shop billing data for (this channel) as "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)" tax ID and ("([^"]+)" country)$/ + */ + public function iSpecifyShopBillingDataAs( + ChannelInterface $channel, + string $company, + string $street, + string $postcode, + string $city, + string $taxId, + CountryInterface $country, + ): void { + $shopBillingDataId = $this->iriConverter->getIriFromResource($channel->getShopBillingData()); + + $this->client->addRequestData( + 'shopBillingData', + [ + '@id' => $shopBillingDataId, + 'company' => $company, + 'street' => $street, + 'postcode' => $postcode, + 'city' => $city, + 'countryCode' => $country->getCode(), + 'taxId' => $taxId, + ], + ); + } + + /** + * @When /^I specify new country code for (this channel) as "([^"]+)"$/ + */ + public function iSpecifyNewCountryCodeForThisChannelAs(ChannelInterface $channel, string $code): void + { + $shopBillingDataId = $this->iriConverter->getIriFromResource($channel->getShopBillingData()); + + $this->client->addRequestData('shopBillingData', ['@id' => $shopBillingDataId, 'countryCode' => $code]); + } + /** * @When I specify shop billing address as :street, :postcode :city, :country */ @@ -180,6 +294,15 @@ final class ManagingChannelsContext implements Context $this->shopBillingData['countryCode'] = $country->getCode(); } + /** + * @Then I save it + */ + public function iSaveIt(): void + { + $this->iAddIt(); + $this->client->update(); + } + /** * @When I select the :taxCalculationStrategy as tax calculation strategy */ @@ -280,6 +403,74 @@ final class ManagingChannelsContext implements Context ); } + /** + * @Then I should not be able to edit its code + */ + public function iShouldNotBeAbleToEditItsCode(): void + { + $this->client->updateRequestData(['code' => 'NEW_CODE']); + + Assert::false($this->responseChecker->hasValue($this->client->update(), 'code', 'NEW_CODE')); + } + + /** + * @Then the base currency field should be disabled + * @Then I should not be able to edit its base currency + */ + public function theBaseCurrencyFieldShouldBeDisabled(): void + { + $this->client->updateRequestData(['baseCurrency' => 'PLN']); + + Assert::false($this->responseChecker->hasValue($this->client->update(), 'baseCurrency', 'PLN')); + } + + /** + * @Then /^(this channel) name should be "([^"]*)"$/ + */ + public function thisChannelNameShouldBe(ChannelInterface $channel, string $name): void + { + Assert::true( + $this->responseChecker->hasValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'name', $name), + sprintf('Its Channel does not have name %s.', $name), + ); + } + + /** + * @Then the :channel channel should no longer exist in the registry + */ + public function theChannelShouldNoLongerExistInTheRegistry(string $name): void + { + Assert::false( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::CHANNELS), 'name', $name), + sprintf('Channel with name %s exists', $name), + ); + } + + /** + * @Then I should be notified that it has been successfully deleted + */ + public function iShouldBeNotifiedThatChannelHasBeenDeleted(): void + { + Assert::true($this->responseChecker->isDeletionSuccessful($this->client->getLastResponse())); + } + + /** + * @Then /^(this channel) menu (taxon should be "([^"]+)")$/ + */ + public function thisChannelMenuTaxonShouldBe(ChannelInterface $channel, TaxonInterface $taxon): void + { + Assert::true( + $this->responseChecker->hasValue( + $this->client->show( + Resources::CHANNELS, + $channel->getCode(), + ), + 'menuTaxon', + $this->sectionAwareIriConverter->getIriFromResourceInSection($taxon, 'admin'), + ), + ); + } + /** * @Then I should see :count channels in the list */ @@ -299,4 +490,225 @@ final class ManagingChannelsContext implements Context $type === 'shipping', )); } + + /** + * @Then I should be notified that it cannot be deleted + */ + public function iShouldBeNotifiedThatItCannotBeDeleted(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'The channel cannot be deleted. At least one enabled channel is required.', + ); + } + + /** + * @Then I should be notified that at least one channel has to be defined + */ + public function iShouldBeNotifiedThatAtLeastOneChannelHasToBeDefined(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Must have at least one enabled entity', + ); + } + + /** + * @Then channel with name :channel should still be enabled + * @Then /^(this channel) should be enabled$/ + */ + public function channelWithNameShouldStillBeEnabled(ChannelInterface $channel): void + { + Assert::true( + $this->responseChecker->hasValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'enabled', true), + sprintf('Channel with name %s does not exists', $channel->getName()), + ); + } + + /** + * @Then this channel should still be named :channel + */ + public function thisChannelShouldStillBeNamed(ChannelInterface $channel): void + { + Assert::true( + $this->responseChecker->hasValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'name', $channel->getName()), + sprintf('Channel with name %s does not exists', $channel->getName()), + ); + } + + /** + * @Then paying in :currency should be possible for the :channel channel + */ + public function payingInCurrencyShouldBePossibleForTheChannel(CurrencyInterface $currency, ChannelInterface $channel): void + { + $currencies = $this->responseChecker->getValue( + $this->client->show(Resources::CHANNELS, $channel->getCode()), + 'currencies', + ); + + Assert::true(in_array($this->sectionAwareIriConverter->getIriFromResourceInSection($currency, 'admin'), $currencies)); + } + + /** + * @Then channel :channel should not have default tax zone + */ + public function channelShouldNotHaveDefaultTaxZone(ChannelInterface $channel): void + { + Assert::same( + $this->responseChecker->getValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'defaultTaxZone'), + null, + sprintf('Channel %s has default tax zone', $channel->getName()), + ); + } + + /** + * @Then the default tax zone for the :channel channel should be :zone + */ + public function theDefaultTaxZoneForTheChannelShouldBe(ChannelInterface $channel, ZoneInterface $zone): void + { + Assert::same( + $this->responseChecker->getValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'defaultTaxZone'), + $this->sectionAwareIriConverter->getIriFromResourceInSection($zone, 'admin'), + sprintf('Channel %s does not have %s default tax zone', $channel->getName(), $zone), + ); + } + + /** + * @Then the channel :channel should be available in :locale + */ + public function theChannelShouldBeAvailableIn(ChannelInterface $channel, LocaleInterface $locale): void + { + $locales = $this->responseChecker->getValue( + $this->client->show(Resources::CHANNELS, $channel->getCode()), + 'locales', + ); + + Assert::true(in_array($this->sectionAwareIriConverter->getIriFromResourceInSection($locale, 'admin'), $locales)); + } + + /** + * @Then I should be notified that the default locale has to be enabled + */ + public function iShouldBeNotifiedThatTheDefaultLocaleHasToBeEnabled(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'defaultLocale: Default locale has to be enabled.', + ); + } + + /** + * @Then /^(this channel) should still be in the registry$/ + */ + public function thisChannelShouldStillBeInTheRegistry(ChannelInterface $channel): void + { + Assert::true( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::CHANNELS), 'code', $channel->getCode()), + sprintf('Channel with code %s does not exists', $channel->getCode()), + ); + } + + /** + * @Then the tax calculation strategy for the :channel channel should be :taxCalculationStrategy + */ + public function theTaxCalculationStrategyForTheChannelShouldBe( + ChannelInterface $channel, + string $taxCalculationStrategy, + ): void { + Assert::same( + $this->responseChecker->getValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'taxCalculationStrategy'), + StringInflector::nameToLowercaseCode($taxCalculationStrategy), + sprintf('Channel %s does not have %s tax calculation strategy', $channel->getName(), $taxCalculationStrategy), + ); + } + + /** + * @Then /^(this channel) should be disabled$/ + */ + public function thisChannelShouldBeDisabled(ChannelInterface $channel): void + { + Assert::same( + $this->responseChecker->getValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'enabled'), + false, + sprintf('Channel %s is enabled', $channel->getName()), + ); + } + + /** + * @Then I should be notified that :element is required + */ + public function iShouldBeNotifiedThatIsRequired(string $element): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('%s: Please enter channel %s.', $element, $element), + ); + } + + /** + * @Then I should be notified that base currency is required + */ + public function iShouldBeNotifiedThatBaseCurrencyIsRequired(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Expected IRI or nested document for attribute "baseCurrency", "NULL" given.', + ); + } + + /** + * @Then I should be notified that default locale is required + */ + public function iShouldBeNotifiedThatDefaultLocaleIsRequired(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Expected IRI or nested document for attribute "defaultLocale", "NULL" given.', + ); + } + + /** + * @Then channel with :element :value should not be added + */ + public function channelWithShouldNotBeAdded(string $element, string $value): void + { + Assert::false( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::CHANNELS), $element, $value), + sprintf('Channel with %s: %s exists', $element, $value), + ); + } + + /** + * @Then I should be notified that channel with this code already exists + */ + public function iShouldBeNotifiedThatChannelWithThisCodeAlreadyExists(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'code: Channel code has to be unique.', + ); + } + + /** + * @Then there should still be only one channel with :element :value + */ + public function thereShouldStillBeOnlyOneChannelWithCode(string $element, string $value): void + { + Assert::same( + count($this->responseChecker->getCollectionItemsWithValue($this->client->index(Resources::CHANNELS), $element, $value)), + 1, + sprintf('There is more than one channel with %s: %s', $element, $value), + ); + } + + /** + * @Then I should be notified that it is not a valid country + */ + public function iShouldBeNotifiedThatItIsNotAValidCountryCode(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'countryCode: This value is not a valid country.', + ); + } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php index e910784eda..4a5b7ad03f 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php @@ -276,7 +276,7 @@ final class ManagingProductsContext implements Context public function iSetItsNonTranslatableAttributeTo(ProductAttributeInterface $attribute, string $value): void { $this->client->addSubResourceData( - 'attributes', + 'attributes', [ 'attribute' => $this->iriConverter->getIriFromResource($attribute), 'value' => $this->getAttributeValueInProperType($attribute, $value), @@ -290,7 +290,7 @@ final class ManagingProductsContext implements Context public function iSetTheInvalidIntegerValueOfTheNonTranslatableAttributeTo(ProductAttributeInterface $attribute, int $value): void { $this->client->addSubResourceData( - 'attributes', + 'attributes', [ 'attribute' => $this->iriConverter->getIriFromResource($attribute), 'value' => $value, @@ -304,7 +304,7 @@ final class ManagingProductsContext implements Context public function iSetTheInvalidStringValueOfTheNonTranslatableAttributeTo(ProductAttributeInterface $attribute, string $value): void { $this->client->addSubResourceData( - 'attributes', + 'attributes', [ 'attribute' => $this->iriConverter->getIriFromResource($attribute), 'value' => $value, @@ -320,7 +320,7 @@ final class ManagingProductsContext implements Context public function iSetItsAttributeTo( ProductAttributeInterface $attribute, ?string $value = null, - string $localeCode = 'en_US' + string $localeCode = 'en_US', ): void { $this->client->addSubResourceData( 'attributes', @@ -363,7 +363,7 @@ final class ManagingProductsContext implements Context public function iSelectValueInForTheAttribute( string $value, string $localeCode, - ProductAttributeInterface $attribute + ProductAttributeInterface $attribute, ): void { $this->client->addSubResourceData( 'attributes', @@ -380,7 +380,7 @@ final class ManagingProductsContext implements Context */ public function iSelectValueForTheAttribute( string $value, - ProductAttributeInterface $attribute + ProductAttributeInterface $attribute, ): void { $this->client->addSubResourceData( 'attributes', @@ -678,7 +678,7 @@ final class ManagingProductsContext implements Context public function nonTranslatableAttributeOfProductShouldBe( ProductAttributeInterface $attribute, ProductInterface $product, - string $value + string $value, ): void { $this->client->show(Resources::PRODUCTS, $product->getCode()); @@ -702,7 +702,7 @@ final class ManagingProductsContext implements Context ProductAttributeInterface $attribute, ProductInterface $product, string $value, - string $localeCode = 'en_US' + string $localeCode = 'en_US', ): void { $this->client->show(Resources::PRODUCTS, $product->getCode()); @@ -718,7 +718,7 @@ final class ManagingProductsContext implements Context foreach ($attributes as $attributeValue) { if ($attributeValue['attribute'] === $this->sectionAwareIriConverter->getIriFromResourceInSection($attribute, 'admin')) { throw new \InvalidArgumentException( - sprintf('Product %s have attribute %s', $product->getName(), $attribute->getName()) + sprintf('Product %s have attribute %s', $product->getName(), $attribute->getName()), ); } } @@ -780,7 +780,7 @@ final class ManagingProductsContext implements Context public function iShouldBeNotifiedThatTheAttributeInShouldBeLongerThan( string $attributeName, string $localeCode, - int $number + int $number, ): void { Assert::contains( $this->responseChecker->getError($this->client->getLastResponse()), @@ -861,7 +861,7 @@ final class ManagingProductsContext implements Context private function getAttributeValueInProperType( ProductAttributeInterface $productAttribute, - string $value + string $value, ): string|bool|float|int { switch ($productAttribute->getStorageType()) { case AttributeValueInterface::STORAGE_BOOLEAN: @@ -887,7 +887,7 @@ final class ManagingProductsContext implements Context } throw new \InvalidArgumentException( - sprintf('Value "%s" not found in attribute "%s"', $value, $attribute->getName()) + sprintf('Value "%s" not found in attribute "%s"', $value, $attribute->getName()), ); } @@ -908,7 +908,7 @@ final class ManagingProductsContext implements Context } throw new \InvalidArgumentException( - sprintf('The given product does not have attribute %s', $attribute->getName()) + sprintf('The given product does not have attribute %s', $attribute->getName()), ); } diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsBillingDataContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsBillingDataContext.php index 302623d43a..42eb04b045 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsBillingDataContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsBillingDataContext.php @@ -24,6 +24,22 @@ final class ManagingChannelsBillingDataContext implements Context { } + /** + * @When I specify shop billing data for this channel as :company, :street, :postcode, :city, :taxId tax ID and :country country + */ + public function iSpecifyNewShopBillingDataForChannelAs( + string $company, + string $street, + string $postcode, + string $city, + string $taxId, + CountryInterface $country, + ): void { + $this->shopBillingDataElement->specifyCompany($company); + $this->shopBillingDataElement->specifyTaxId($taxId); + $this->shopBillingDataElement->specifyBillingAddress($street, $postcode, $city, $country->getCode()); + } + /** * @When I specify company as :company */ @@ -69,6 +85,7 @@ final class ManagingChannelsBillingDataContext implements Context } /** + * @Then this channel shop billing address should be :street, :postcode :city and :country country * @Then this channel shop billing address should be :street, :postcode :city, :country */ public function thisChannelShopBillingAddressShouldBe( diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsContext.php index a44d18bb00..d1d43bd43e 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsContext.php @@ -292,6 +292,7 @@ final class ManagingChannelsContext implements Context * @Given I am modifying a channel :channel * @When I want to modify a channel :channel * @When /^I want to modify (this channel)$/ + * @When I want to modify a billing data of channel :channel */ public function iWantToModifyChannel(ChannelInterface $channel): void { @@ -315,6 +316,7 @@ final class ManagingChannelsContext implements Context /** * @When I save my changes * @When I try to save my changes + * @When I save it */ public function iSaveMyChanges(): void { @@ -332,7 +334,7 @@ final class ManagingChannelsContext implements Context /** * @Then there should still be only one channel with :element :value */ - public function thereShouldStillBeOnlyOneChannelWithCode(string $element, string $value) + public function thereShouldStillBeOnlyOneChannelWithCode(string $element, string $value): void { $this->iWantToBrowseChannels(); @@ -375,6 +377,7 @@ final class ManagingChannelsContext implements Context /** * @Then the code field should be disabled + * @Then I should not be able to edit its code */ public function theCodeFieldShouldBeDisabled(): void { @@ -461,7 +464,7 @@ final class ManagingChannelsContext implements Context /** * @Then paying in :currencyCode should be possible for the :channel channel */ - public function payingInEuroShouldBePossibleForTheChannel(string $currencyCode, ChannelInterface $channel): void + public function payingInCurrencyShouldBePossibleForTheChannel(string $currencyCode, ChannelInterface $channel): void { $this->updatePage->open(['id' => $channel->getId()]); @@ -607,6 +610,7 @@ final class ManagingChannelsContext implements Context /** * @Then the base currency field should be disabled + * @Then I should not be able to edit its base currency */ public function theBaseCurrencyFieldShouldBeDisabled(): void { diff --git a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml index 1d0db51420..ef996dd029 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml @@ -64,6 +64,12 @@ + + + + + + diff --git a/src/Sylius/Behat/Resources/config/suites/api/channel/managing_channels.yml b/src/Sylius/Behat/Resources/config/suites/api/channel/managing_channels.yml index 924a4300d3..1f68d15ceb 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/channel/managing_channels.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/channel/managing_channels.yml @@ -29,6 +29,7 @@ default: - sylius.behat.context.api.admin.managing_channels - sylius.behat.context.api.admin.response - sylius.behat.context.api.admin.save + - Sylius\Behat\Context\Api\Admin\ManagingChannelsBillingDataContext - Sylius\Behat\Context\Api\Admin\ManagingChannelPriceHistoryConfigsContext filters: diff --git a/src/Sylius/Bundle/AddressingBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/AddressingBundle/DependencyInjection/Configuration.php index fed7c017d7..3851b89526 100644 --- a/src/Sylius/Bundle/AddressingBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/AddressingBundle/DependencyInjection/Configuration.php @@ -14,6 +14,10 @@ declare(strict_types=1); namespace Sylius\Bundle\AddressingBundle\DependencyInjection; use Sylius\Bundle\AddressingBundle\Controller\ProvinceController; +use Sylius\Bundle\AddressingBundle\Doctrine\ORM\AddressRepository; +use Sylius\Bundle\AddressingBundle\Doctrine\ORM\CountryRepository; +use Sylius\Bundle\AddressingBundle\Doctrine\ORM\ProvinceRepository; +use Sylius\Bundle\AddressingBundle\Doctrine\ORM\ZoneMemberRepository; use Sylius\Bundle\AddressingBundle\Form\Type\AddressType; use Sylius\Bundle\AddressingBundle\Form\Type\CountryType; use Sylius\Bundle\AddressingBundle\Form\Type\ProvinceType; @@ -77,7 +81,7 @@ final class Configuration implements ConfigurationInterface ->scalarNode('model')->defaultValue(Address::class)->cannotBeEmpty()->end() ->scalarNode('interface')->defaultValue(AddressInterface::class)->cannotBeEmpty()->end() ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() - ->scalarNode('repository')->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(AddressRepository::class)->cannotBeEmpty()->end() ->scalarNode('factory')->defaultValue(Factory::class)->end() ->scalarNode('form')->defaultValue(AddressType::class)->cannotBeEmpty()->end() ->end() @@ -109,7 +113,7 @@ final class Configuration implements ConfigurationInterface ->scalarNode('model')->defaultValue(Country::class)->cannotBeEmpty()->end() ->scalarNode('interface')->defaultValue(CountryInterface::class)->cannotBeEmpty()->end() ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() - ->scalarNode('repository')->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(CountryRepository::class)->cannotBeEmpty()->end() ->scalarNode('factory')->defaultValue(Factory::class)->end() ->scalarNode('form')->defaultValue(CountryType::class)->cannotBeEmpty()->end() ->end() @@ -126,7 +130,7 @@ final class Configuration implements ConfigurationInterface ->scalarNode('model')->defaultValue(Province::class)->cannotBeEmpty()->end() ->scalarNode('interface')->defaultValue(ProvinceInterface::class)->cannotBeEmpty()->end() ->scalarNode('controller')->defaultValue(ProvinceController::class)->cannotBeEmpty()->end() - ->scalarNode('repository')->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(ProvinceRepository::class)->cannotBeEmpty()->end() ->scalarNode('factory')->defaultValue(Factory::class)->end() ->scalarNode('form')->defaultValue(ProvinceType::class)->cannotBeEmpty()->end() ->end() @@ -160,7 +164,7 @@ final class Configuration implements ConfigurationInterface ->scalarNode('model')->defaultValue(ZoneMember::class)->cannotBeEmpty()->end() ->scalarNode('interface')->defaultValue(ZoneMemberInterface::class)->cannotBeEmpty()->end() ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() - ->scalarNode('repository')->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(ZoneMemberRepository::class)->cannotBeEmpty()->end() ->scalarNode('factory')->defaultValue(Factory::class)->end() ->scalarNode('form')->defaultValue(ZoneMemberType::class)->cannotBeEmpty()->end() ->end() diff --git a/src/Sylius/Bundle/AddressingBundle/Doctrine/ORM/AddressRepository.php b/src/Sylius/Bundle/AddressingBundle/Doctrine/ORM/AddressRepository.php new file mode 100644 index 0000000000..28ec20afd9 --- /dev/null +++ b/src/Sylius/Bundle/AddressingBundle/Doctrine/ORM/AddressRepository.php @@ -0,0 +1,25 @@ + + */ +class AddressRepository extends EntityRepository implements AddressRepositoryInterface +{ +} diff --git a/src/Sylius/Bundle/AddressingBundle/Doctrine/ORM/CountryRepository.php b/src/Sylius/Bundle/AddressingBundle/Doctrine/ORM/CountryRepository.php new file mode 100644 index 0000000000..0586511909 --- /dev/null +++ b/src/Sylius/Bundle/AddressingBundle/Doctrine/ORM/CountryRepository.php @@ -0,0 +1,25 @@ + + */ +class CountryRepository extends EntityRepository implements CountryRepositoryInterface +{ +} diff --git a/src/Sylius/Bundle/AddressingBundle/Doctrine/ORM/ProvinceRepository.php b/src/Sylius/Bundle/AddressingBundle/Doctrine/ORM/ProvinceRepository.php new file mode 100644 index 0000000000..0aa9ef408e --- /dev/null +++ b/src/Sylius/Bundle/AddressingBundle/Doctrine/ORM/ProvinceRepository.php @@ -0,0 +1,25 @@ + + */ +class ProvinceRepository extends EntityRepository implements ProvinceRepositoryInterface +{ +} diff --git a/src/Sylius/Bundle/AddressingBundle/Doctrine/ORM/ZoneMemberRepository.php b/src/Sylius/Bundle/AddressingBundle/Doctrine/ORM/ZoneMemberRepository.php new file mode 100644 index 0000000000..5632ed0477 --- /dev/null +++ b/src/Sylius/Bundle/AddressingBundle/Doctrine/ORM/ZoneMemberRepository.php @@ -0,0 +1,25 @@ + + */ +class ZoneMemberRepository extends EntityRepository implements ZoneMemberRepositoryInterface +{ +} diff --git a/src/Sylius/Bundle/AdminBundle/Action/Account/RenderResetPasswordPageAction.php b/src/Sylius/Bundle/AdminBundle/Action/Account/RenderResetPasswordPageAction.php index cef6f5aedd..f144e9a4cd 100644 --- a/src/Sylius/Bundle/AdminBundle/Action/Account/RenderResetPasswordPageAction.php +++ b/src/Sylius/Bundle/AdminBundle/Action/Account/RenderResetPasswordPageAction.php @@ -72,7 +72,7 @@ final readonly class RenderResetPasswordPageAction ->add('error', 'sylius.admin.password_reset.token_expired') ; - $attributes = $request->attributes->get('_sylius'); + $attributes = $request->attributes->get('_sylius', []); $redirect = $attributes['redirect'] ?? 'sylius_admin_login'; if (is_array($redirect)) { diff --git a/src/Sylius/Bundle/AdminBundle/Action/Account/RequestPasswordResetAction.php b/src/Sylius/Bundle/AdminBundle/Action/Account/RequestPasswordResetAction.php index 50aa89368e..acaf627876 100644 --- a/src/Sylius/Bundle/AdminBundle/Action/Account/RequestPasswordResetAction.php +++ b/src/Sylius/Bundle/AdminBundle/Action/Account/RequestPasswordResetAction.php @@ -56,7 +56,7 @@ final readonly class RequestPasswordResetAction ->add('success', 'sylius.admin.request_reset_password.success') ; - $options = $request->attributes->get('_sylius'); + $options = $request->attributes->get('_sylius', []); $redirectRoute = $options['redirect'] ?? 'sylius_admin_login'; if (is_array($redirectRoute)) { diff --git a/src/Sylius/Bundle/AdminBundle/Action/Account/ResetPasswordAction.php b/src/Sylius/Bundle/AdminBundle/Action/Account/ResetPasswordAction.php index db1e8df216..61421f555a 100644 --- a/src/Sylius/Bundle/AdminBundle/Action/Account/ResetPasswordAction.php +++ b/src/Sylius/Bundle/AdminBundle/Action/Account/ResetPasswordAction.php @@ -52,7 +52,7 @@ final class ResetPasswordAction ->add('success', 'sylius.admin.password_reset.success') ; - $attributes = $request->attributes->get('_sylius'); + $attributes = $request->attributes->get('_sylius', []); $redirect = $attributes['redirect'] ?? 'sylius_admin_login'; if (is_array($redirect)) { diff --git a/src/Sylius/Bundle/AdminBundle/Action/RemoveAvatarAction.php b/src/Sylius/Bundle/AdminBundle/Action/RemoveAvatarAction.php index 8f65e90820..99a39f710b 100644 --- a/src/Sylius/Bundle/AdminBundle/Action/RemoveAvatarAction.php +++ b/src/Sylius/Bundle/AdminBundle/Action/RemoveAvatarAction.php @@ -34,9 +34,11 @@ final class RemoveAvatarAction public function __invoke(Request $request): Response { - $userId = $request->attributes->get('id'); + $userId = $request->attributes->get('id', ''); - if (!$this->csrfTokenManager->isTokenValid(new CsrfToken($userId, (string) $request->query->get('_csrf_token')))) { + if (!$this->csrfTokenManager->isTokenValid( + new CsrfToken($userId, (string) $request->query->get('_csrf_token', '')), + )) { throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.'); } diff --git a/src/Sylius/Bundle/AdminBundle/Action/ResendOrderConfirmationEmailAction.php b/src/Sylius/Bundle/AdminBundle/Action/ResendOrderConfirmationEmailAction.php index f231abfda9..02441a1d4f 100644 --- a/src/Sylius/Bundle/AdminBundle/Action/ResendOrderConfirmationEmailAction.php +++ b/src/Sylius/Bundle/AdminBundle/Action/ResendOrderConfirmationEmailAction.php @@ -38,9 +38,11 @@ final readonly class ResendOrderConfirmationEmailAction public function __invoke(Request $request): Response { - $orderId = $request->attributes->get('id'); + $orderId = $request->attributes->get('id', ''); - if (!$this->csrfTokenManager->isTokenValid(new CsrfToken($orderId, (string) $request->query->get('_csrf_token')))) { + if (!$this->csrfTokenManager->isTokenValid( + new CsrfToken($orderId, (string) $request->query->get('_csrf_token', '')), + )) { throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.'); } diff --git a/src/Sylius/Bundle/AdminBundle/Action/ResendShipmentConfirmationEmailAction.php b/src/Sylius/Bundle/AdminBundle/Action/ResendShipmentConfirmationEmailAction.php index d8034defab..6a6eebfc42 100644 --- a/src/Sylius/Bundle/AdminBundle/Action/ResendShipmentConfirmationEmailAction.php +++ b/src/Sylius/Bundle/AdminBundle/Action/ResendShipmentConfirmationEmailAction.php @@ -38,9 +38,11 @@ final readonly class ResendShipmentConfirmationEmailAction public function __invoke(Request $request): Response { - $shipmentId = $request->attributes->get('id'); + $shipmentId = $request->attributes->get('id', ''); - if (!$this->csrfTokenManager->isTokenValid(new CsrfToken($shipmentId, (string) $request->query->get('_csrf_token')))) { + if (!$this->csrfTokenManager->isTokenValid( + new CsrfToken($shipmentId, (string) $request->query->get('_csrf_token', '')), + )) { throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.'); } diff --git a/src/Sylius/Bundle/AdminBundle/Controller/RemoveCatalogPromotionAction.php b/src/Sylius/Bundle/AdminBundle/Controller/RemoveCatalogPromotionAction.php index 20bb2c5f58..6f48ac30e6 100644 --- a/src/Sylius/Bundle/AdminBundle/Controller/RemoveCatalogPromotionAction.php +++ b/src/Sylius/Bundle/AdminBundle/Controller/RemoveCatalogPromotionAction.php @@ -31,8 +31,13 @@ final class RemoveCatalogPromotionAction public function __invoke(Request $request): Response { + $catalogPromotionCode = $request->attributes->get('code'); + if (null === $catalogPromotionCode) { + throw new NotFoundHttpException('The catalog promotion has not been found'); + } + try { - $this->catalogPromotionRemovalProcessor->removeCatalogPromotion($request->attributes->get('code')); + $this->catalogPromotionRemovalProcessor->removeCatalogPromotion($catalogPromotionCode); /** @var Session $session */ $session = $request->getSession(); diff --git a/src/Sylius/Bundle/AdminBundle/EventListener/AdminFilterSubscriber.php b/src/Sylius/Bundle/AdminBundle/EventListener/AdminFilterSubscriber.php index dfb114f286..32305a7e53 100644 --- a/src/Sylius/Bundle/AdminBundle/EventListener/AdminFilterSubscriber.php +++ b/src/Sylius/Bundle/AdminBundle/EventListener/AdminFilterSubscriber.php @@ -44,17 +44,12 @@ final class AdminFilterSubscriber implements EventSubscriberInterface } $requestAttributes = $eventRequest->attributes; - $originalRoute = $requestAttributes->get('_route'); - if (!$this->isIndexResourceRoute($originalRoute)) { - return; - } - - if (!$this->isAdminSection($requestAttributes->get('_sylius', []))) { - return; - } - - if (null === $requestAttributes->get('_controller')) { + if ( + null === $requestAttributes->get('_controller') || + !$this->isIndexResourceRoute($requestAttributes->get('_route', '')) || + !$this->isAdminSection($requestAttributes->get('_sylius', [])) + ) { return; } @@ -80,6 +75,6 @@ final class AdminFilterSubscriber implements EventSubscriberInterface private function isAdminSection(array $syliusParameters): bool { - return array_key_exists('section', $syliusParameters) && 'admin' === $syliusParameters['section']; + return isset($syliusParameters['section']) && 'admin' === $syliusParameters['section']; } } diff --git a/src/Sylius/Bundle/AdminBundle/EventListener/ResourceDeleteSubscriber.php b/src/Sylius/Bundle/AdminBundle/EventListener/ResourceDeleteSubscriber.php index 024dceaab2..83128a778f 100644 --- a/src/Sylius/Bundle/AdminBundle/EventListener/ResourceDeleteSubscriber.php +++ b/src/Sylius/Bundle/AdminBundle/EventListener/ResourceDeleteSubscriber.php @@ -58,7 +58,7 @@ final readonly class ResourceDeleteSubscriber implements EventSubscriberInterfac $eventRequest = $event->getRequest(); $requestAttributes = $eventRequest->attributes; - $originalRoute = $requestAttributes->get('_route'); + $originalRoute = $requestAttributes->get('_route', ''); if (!$this->isMethodDelete($eventRequest) || !$this->isSyliusRoute($originalRoute) || diff --git a/src/Sylius/Bundle/AdminBundle/Tests/MessageHandler/Admin/SendResetPasswordEmailHandlerTest.php b/src/Sylius/Bundle/AdminBundle/Tests/MessageHandler/Admin/SendResetPasswordEmailHandlerTest.php index cca655a0a9..fedcc1aaa4 100644 --- a/src/Sylius/Bundle/AdminBundle/Tests/MessageHandler/Admin/SendResetPasswordEmailHandlerTest.php +++ b/src/Sylius/Bundle/AdminBundle/Tests/MessageHandler/Admin/SendResetPasswordEmailHandlerTest.php @@ -16,24 +16,16 @@ namespace Sylius\Bundle\AdminBundle\Tests\MessageHandler\Admin; use Sylius\Bundle\CoreBundle\Message\Admin\Account\SendResetPasswordEmail; use Sylius\Bundle\CoreBundle\MessageHandler\Admin\Account\SendResetPasswordEmailHandler; use Sylius\Component\Core\Model\AdminUser; -use Sylius\Component\Core\Test\SwiftmailerAssertionTrait; use Sylius\Component\Mailer\Sender\SenderInterface; use Sylius\Component\User\Repository\UserRepositoryInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Component\Filesystem\Filesystem; use Symfony\Contracts\Translation\TranslatorInterface; final class SendResetPasswordEmailHandlerTest extends KernelTestCase { - use SwiftmailerAssertionTrait; - /** @test */ public function it_sends_password_reset_token_email(): void { - if (self::isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment without swiftmailer'); - } - $container = self::bootKernel()->getContainer(); /** @var TranslatorInterface $translator */ @@ -67,57 +59,4 @@ final class SendResetPasswordEmailHandlerTest extends KernelTestCase $translator->trans('sylius.email.admin_password_reset.to_reset_your_password', [], null, 'en_US'), ); } - - /** @test */ - public function it_sends_password_reset_token_email_with_swiftmailer(): void - { - if (!self::isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment with swiftmailer'); - } - - $container = self::bootKernel()->getContainer(); - - self::setSpoolDirectory($container->getParameter('kernel.cache_dir') . '/spool'); - - /** @var Filesystem $filesystem */ - $filesystem = $container->get('test.filesystem.public'); - - $filesystem->remove(self::getSpoolDirectory()); - - /** @var TranslatorInterface $translator */ - $translator = $container->get('translator'); - - /** @var SenderInterface $emailSender */ - $emailSender = $container->get('sylius.email_sender'); - - $adminUser = new AdminUser(); - $adminUser->setEmail('sylius@example.com'); - $adminUser->setPasswordResetToken('my_reset_token'); - - $adminUserRepository = $this->createMock(UserRepositoryInterface::class); - $adminUserRepository - ->method('findOneByEmail') - ->with('sylius@example.com') - ->willReturn($adminUser) - ; - - $resetPasswordEmailHandler = new SendResetPasswordEmailHandler($adminUserRepository, $emailSender); - $resetPasswordEmailHandler(new SendResetPasswordEmail( - 'sylius@example.com', - 'en_US', - )); - - self::assertSpooledMessagesCountWithRecipient(1, 'sylius@example.com'); - self::assertSpooledMessageWithContentHasRecipient( - $translator->trans('sylius.email.admin_password_reset.to_reset_your_password', [], null, 'en_US'), - 'sylius@example.com', - ); - } - - private static function isItSwiftmailerTestEnv(): bool - { - $env = self::getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } } diff --git a/src/Sylius/Bundle/AdminBundle/spec/Action/Account/RequestPasswordResetActionSpec.php b/src/Sylius/Bundle/AdminBundle/spec/Action/Account/RequestPasswordResetActionSpec.php index ec1aa68bd8..2016713938 100644 --- a/src/Sylius/Bundle/AdminBundle/spec/Action/Account/RequestPasswordResetActionSpec.php +++ b/src/Sylius/Bundle/AdminBundle/spec/Action/Account/RequestPasswordResetActionSpec.php @@ -81,7 +81,7 @@ final class RequestPasswordResetActionSpec extends ObjectBehavior $flashBag->add('success', 'sylius.admin.request_reset_password.success')->shouldBeCalled(); - $attributesBag->get('_sylius')->shouldBeCalled()->willReturn([ + $attributesBag->get('_sylius', [])->shouldBeCalled()->willReturn([ 'redirect' => 'my_custom_route', ]); $request->attributes = $attributesBag->getWrappedObject(); @@ -120,7 +120,7 @@ final class RequestPasswordResetActionSpec extends ObjectBehavior $parameters = [ 'my_parameter' => 'my_value', ]; - $attributesBag->get('_sylius')->shouldBeCalled()->willReturn([ + $attributesBag->get('_sylius', [])->shouldBeCalled()->willReturn([ 'redirect' => [ 'route' => $route, 'params' => $parameters, @@ -158,7 +158,7 @@ final class RequestPasswordResetActionSpec extends ObjectBehavior $flashBag->add('success', 'sylius.admin.request_reset_password.success')->shouldBeCalled(); - $attributesBag->get('_sylius')->shouldBeCalled()->willReturn(null); + $attributesBag->get('_sylius', [])->shouldBeCalled()->willReturn([]); $request->attributes = $attributesBag->getWrappedObject(); $router->generate('sylius_admin_login')->shouldBeCalled()->willReturn('/login'); diff --git a/src/Sylius/Bundle/AdminBundle/spec/EventListener/AdminFilterSubscriberSpec.php b/src/Sylius/Bundle/AdminBundle/spec/EventListener/AdminFilterSubscriberSpec.php index 5d8266fae7..e151760f8a 100644 --- a/src/Sylius/Bundle/AdminBundle/spec/EventListener/AdminFilterSubscriberSpec.php +++ b/src/Sylius/Bundle/AdminBundle/spec/EventListener/AdminFilterSubscriberSpec.php @@ -43,7 +43,7 @@ final class AdminFilterSubscriberSpec extends ObjectBehavior $event->isMainRequest()->willReturn(true); $request->getRequestFormat()->willReturn('html'); - $attributes->get('_route')->willReturn('sylius_admin_product_index'); + $attributes->get('_route', '')->willReturn('sylius_admin_product_index'); $attributes->get('_sylius', [])->willReturn(['section' => 'admin']); $attributes->get('_controller')->willReturn('Sylius\Bundle\AdminBundle\Controller\ProductController::indexAction'); $request->attributes = $attributes; @@ -82,7 +82,7 @@ final class AdminFilterSubscriberSpec extends ObjectBehavior $event->isMainRequest()->willReturn(true); $request->getRequestFormat()->willReturn('json'); - $attributes->get('_route')->willReturn('sylius_admin_product_index'); + $attributes->get('_route', '')->willReturn('sylius_admin_product_index'); $attributes->get('_sylius', [])->willReturn(['section' => 'admin']); $attributes->get('_controller')->willReturn('Sylius\Bundle\AdminBundle\Controller\ProductController::indexAction'); $request->attributes = $attributes; @@ -109,7 +109,7 @@ final class AdminFilterSubscriberSpec extends ObjectBehavior $event->isMainRequest()->willReturn(true); $request->getRequestFormat()->willReturn('json'); - $attributes->get('_route')->willReturn('sylius_admin_product_index'); + $attributes->get('_route', '')->willReturn('sylius_admin_product_index'); $attributes->get('_sylius', [])->willReturn(['section' => 'shop']); $attributes->get('_controller')->willReturn('Sylius\Bundle\AdminBundle\Controller\ProductController::indexAction'); $request->attributes = $attributes; @@ -136,7 +136,7 @@ final class AdminFilterSubscriberSpec extends ObjectBehavior $event->isMainRequest()->willReturn(true); $request->getRequestFormat()->willReturn('json'); - $attributes->get('_route')->willReturn('sylius_admin_product_index'); + $attributes->get('_route', '')->willReturn('sylius_admin_product_index'); $attributes->get('_sylius', [])->willReturn(['section' => 'shop']); $attributes->get('_controller')->willReturn(null); $request->attributes = $attributes; @@ -153,6 +153,33 @@ final class AdminFilterSubscriberSpec extends ObjectBehavior $this->onKernelRequest($event); } + function it_does_not_add_filter_to_filter_storage_if_route_is_missing( + RequestEvent $event, + Request $request, + ParameterBag $attributes, + ParameterBag $query, + FilterStorageInterface $filterStorage, + ): void { + $event->isMainRequest()->willReturn(true); + $request->getRequestFormat()->willReturn('json'); + + $attributes->get('_route', '')->willReturn(''); + $attributes->get('_sylius', [])->willReturn(['section' => 'shop']); + $attributes->get('_controller')->willReturn('Sylius\Bundle\AdminBundle\Controller\ProductController::indexAction'); + $request->attributes = $attributes; + + $query->all()->willReturn(['filter' => 'foo']); + $request->query = $query; + + $filterStorage->all()->willReturn([]); + + $event->getRequest()->willReturn($request); + + $filterStorage->set(Argument::any())->shouldNotBeCalled(); + + $this->onKernelRequest($event); + } + function it_does_not_add_filter_to_filter_storage_if_it_is_not_a_index_resource_route( RequestEvent $event, Request $request, @@ -163,7 +190,7 @@ final class AdminFilterSubscriberSpec extends ObjectBehavior $event->isMainRequest()->willReturn(true); $request->getRequestFormat()->willReturn('json'); - $attributes->get('_route')->willReturn('sylius_admin_product_update'); + $attributes->get('_route', '')->willReturn('sylius_admin_product_update'); $attributes->get('_sylius', [])->willReturn(['section' => 'shop']); $attributes->get('_controller')->willReturn('Sylius\Bundle\AdminBundle\Controller\ProductController::indexAction'); $request->attributes = $attributes; diff --git a/src/Sylius/Bundle/AdminBundle/test/config/bundles.php b/src/Sylius/Bundle/AdminBundle/test/config/bundles.php index b5af30ce91..4f3ef19851 100644 --- a/src/Sylius/Bundle/AdminBundle/test/config/bundles.php +++ b/src/Sylius/Bundle/AdminBundle/test/config/bundles.php @@ -14,7 +14,6 @@ declare(strict_types=1); return [ Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], - Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['test_with_swiftmailer' => true], Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], diff --git a/src/Sylius/Bundle/AdminBundle/test/config/packages/test_with_swiftmailer/filesystem.yaml b/src/Sylius/Bundle/AdminBundle/test/config/packages/test_with_swiftmailer/filesystem.yaml deleted file mode 100644 index 1828fd69a2..0000000000 --- a/src/Sylius/Bundle/AdminBundle/test/config/packages/test_with_swiftmailer/filesystem.yaml +++ /dev/null @@ -1,4 +0,0 @@ -services: - test.filesystem.public: - alias: filesystem - public: true diff --git a/src/Sylius/Bundle/AdminBundle/test/config/packages/test_with_swiftmailer/swiftmailer.yaml b/src/Sylius/Bundle/AdminBundle/test/config/packages/test_with_swiftmailer/swiftmailer.yaml deleted file mode 100644 index c438f4b256..0000000000 --- a/src/Sylius/Bundle/AdminBundle/test/config/packages/test_with_swiftmailer/swiftmailer.yaml +++ /dev/null @@ -1,6 +0,0 @@ -swiftmailer: - disable_delivery: true - logging: true - spool: - type: file - path: "%kernel.cache_dir%/spool" diff --git a/src/Sylius/Bundle/AdminBundle/test/config/packages/test_with_swiftmailer/sylius_mailer.yaml b/src/Sylius/Bundle/AdminBundle/test/config/packages/test_with_swiftmailer/sylius_mailer.yaml deleted file mode 100644 index e2310f20bf..0000000000 --- a/src/Sylius/Bundle/AdminBundle/test/config/packages/test_with_swiftmailer/sylius_mailer.yaml +++ /dev/null @@ -1,2 +0,0 @@ -sylius_mailer: - sender_adapter: sylius.email_sender.adapter.swiftmailer diff --git a/src/Sylius/Bundle/ApiBundle/Controller/DeleteOrderItemAction.php b/src/Sylius/Bundle/ApiBundle/Controller/DeleteOrderItemAction.php index 3e872f3e6c..f02d72d503 100644 --- a/src/Sylius/Bundle/ApiBundle/Controller/DeleteOrderItemAction.php +++ b/src/Sylius/Bundle/ApiBundle/Controller/DeleteOrderItemAction.php @@ -33,9 +33,11 @@ final readonly class DeleteOrderItemAction { $orderItemId = $request->attributes->get('itemId'); $tokenValue = $request->attributes->get('tokenValue'); + if (null === $orderItemId || null === $tokenValue) { + throw new OrderItemNotFoundException(); + } $orderItem = $this->orderItemRepository->findOneByIdAndCartTokenValue($orderItemId, $tokenValue); - if ($orderItem === null) { throw new OrderItemNotFoundException(); } diff --git a/src/Sylius/Bundle/ApiBundle/Controller/Payment/GetPaymentConfiguration.php b/src/Sylius/Bundle/ApiBundle/Controller/Payment/GetPaymentConfiguration.php index 0d74eea608..559ad546be 100644 --- a/src/Sylius/Bundle/ApiBundle/Controller/Payment/GetPaymentConfiguration.php +++ b/src/Sylius/Bundle/ApiBundle/Controller/Payment/GetPaymentConfiguration.php @@ -31,11 +31,14 @@ final readonly class GetPaymentConfiguration public function __invoke(Request $request): JsonResponse { + $paymentId = $request->attributes->get('paymentId'); + $tokenValue = $request->attributes->get('tokenValue'); + if (null === $paymentId || null === $tokenValue) { + throw new PaymentNotFoundException(); + } + /** @var PaymentInterface|null $payment */ - $payment = $this->paymentRepository->findOneByOrderToken( - $request->attributes->get('paymentId'), - $request->attributes->get('tokenValue'), - ); + $payment = $this->paymentRepository->findOneByOrderToken($paymentId, $tokenValue); if ($payment === null) { throw new PaymentNotFoundException(); diff --git a/src/Sylius/Bundle/ApiBundle/Controller/RemoveCatalogPromotionAction.php b/src/Sylius/Bundle/ApiBundle/Controller/RemoveCatalogPromotionAction.php index bbc97b1f65..93f8ee45f5 100644 --- a/src/Sylius/Bundle/ApiBundle/Controller/RemoveCatalogPromotionAction.php +++ b/src/Sylius/Bundle/ApiBundle/Controller/RemoveCatalogPromotionAction.php @@ -30,8 +30,13 @@ final readonly class RemoveCatalogPromotionAction public function __invoke(Request $request): Response { + $catalogPromotionCode = $request->attributes->get('code'); + if (null === $catalogPromotionCode) { + return new JsonResponse(status: Response::HTTP_NOT_FOUND); + } + try { - $this->catalogPromotionRemovalProcessor->removeCatalogPromotion($request->attributes->get('code')); + $this->catalogPromotionRemovalProcessor->removeCatalogPromotion($catalogPromotionCode); return new Response(status: Response::HTTP_ACCEPTED); } catch (CatalogPromotionNotFoundException) { diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/ChannelDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/ChannelDataPersister.php new file mode 100644 index 0000000000..ee070bf845 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/ChannelDataPersister.php @@ -0,0 +1,56 @@ + $context + */ + public function supports($data, array $context = []): bool + { + return $data instanceof ChannelInterface; + } + + /** + * @param array $context + */ + public function persist($data, array $context = []) + { + return $this->decoratedDataPersister->persist($data, $context); + } + + /** + * @param array $context + */ + public function remove($data, array $context = []): void + { + if (!$this->channelDeletionChecker->isDeletable($data)) { + throw new ChannelCannotBeRemoved('The channel cannot be deleted. At least one enabled channel is required.'); + } + + $this->decoratedDataPersister->remove($data, $context); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/TranslatableDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/TranslatableDataPersister.php new file mode 100644 index 0000000000..6111461ac3 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/TranslatableDataPersister.php @@ -0,0 +1,67 @@ + $context + */ + public function supports($data, array $context = []): bool + { + return $data instanceof TranslatableInterface; + } + + /** + * @param TranslatableInterface $data + * @param array $context + */ + public function persist($data, array $context = []): object + { + $defaultLocaleCode = $this->localeProvider->getDefaultLocaleCode(); + + if (!$data->getTranslations()->containsKey($defaultLocaleCode)) { + throw new TranslationInDefaultLocaleCannotBeRemoved( + sprintf('Translation in the default locale "%s" cannot be removed.', $defaultLocaleCode), + ); + } + + return $data; + } + + /** + * @param TranslatableInterface $data + * @param array $context + */ + public function remove($data, array $context = []): mixed + { + return $data; + } + + /** @param array $context */ + public function resumable(array $context = []): bool + { + return true; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Exception/ChannelCannotBeRemoved.php b/src/Sylius/Bundle/ApiBundle/Exception/ChannelCannotBeRemoved.php new file mode 100644 index 0000000000..46827bdc1c --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Exception/ChannelCannotBeRemoved.php @@ -0,0 +1,26 @@ + sylius + + + DELETE + /admin/channels/{code} + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/Product.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/Product.xml index 84a63872ea..7a8812d169 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/Product.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/Product.xml @@ -146,7 +146,6 @@ string string - string diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ProductAssociationType.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ProductAssociationType.xml index 36fded64b4..566e9db8e6 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ProductAssociationType.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ProductAssociationType.xml @@ -87,7 +87,6 @@ string string - string diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ProductOption.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ProductOption.xml index 916115da52..a2e953c1ff 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ProductOption.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ProductOption.xml @@ -95,7 +95,6 @@ string - string diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ProductVariant.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ProductVariant.xml index 43e9ebc768..2617661fa6 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ProductVariant.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ProductVariant.xml @@ -99,7 +99,6 @@ string - string diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ShippingMethod.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ShippingMethod.xml index 4b48374b6d..d482e8dc62 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ShippingMethod.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/ShippingMethod.xml @@ -140,7 +140,6 @@ string string - string diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/Taxon.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/Taxon.xml index 0993184258..aa26ccb3e1 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/Taxon.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/legacy_api_resources/Taxon.xml @@ -85,7 +85,6 @@ string string - string diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Channel.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Channel.xml index 41e7be26a2..9e076c196e 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Channel.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Channel.xml @@ -24,23 +24,28 @@ admin:channel:read admin:channel:create + admin:channel:update shop:channel:read admin:channel:read admin:channel:create + admin:channel:update admin:channel:read admin:channel:create + admin:channel:update admin:channel:read admin:channel:create + admin:channel:update admin:channel:read admin:channel:create + admin:channel:update admin:channel:read @@ -50,53 +55,65 @@ admin:channel:read admin:channel:create + admin:channel:update shop:channel:read admin:channel:read admin:channel:create + admin:channel:update admin:channel:read admin:channel:create + admin:channel:update admin:channel:read admin:channel:create + admin:channel:update shop:channel:read admin:channel:read admin:channel:create + admin:channel:update shop:channel:read admin:channel:read admin:channel:create + admin:channel:update admin:channel:read admin:channel:create + admin:channel:update admin:channel:read admin:channel:create + admin:channel:update admin:channel:read admin:channel:create + admin:channel:update admin:channel:read admin:channel:create + admin:channel:update admin:channel:read admin:channel:create + admin:channel:update admin:channel:read admin:channel:create + admin:channel:update admin:channel:read @@ -106,14 +123,17 @@ admin:channel:read admin:channel:create + admin:channel:update admin:channel:read admin:channel:create + admin:channel:update admin:channel:read admin:channel:create + admin:channel:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShopBillingData.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShopBillingData.xml index 06ab5ff099..05c504e196 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShopBillingData.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShopBillingData.xml @@ -21,26 +21,32 @@ admin:channel:create + admin:channel:update admin:shop_billing_data:read admin:channel:create + admin:channel:update admin:shop_billing_data:read admin:channel:create + admin:channel:update admin:shop_billing_data:read admin:channel:create + admin:channel:update admin:shop_billing_data:read admin:channel:create + admin:channel:update admin:shop_billing_data:read admin:channel:create + admin:channel:update admin:shop_billing_data:read diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml index 24bfa7a41e..fa7d1b4531 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml @@ -31,6 +31,12 @@ + + + + + + @@ -74,6 +80,11 @@ + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml index 11144f55b8..18fb273da3 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml @@ -105,6 +105,11 @@ + + + + + Y-m-d H:i:s @@ -122,12 +127,17 @@ + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/Channel.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/Channel.xml new file mode 100644 index 0000000000..eae9231cdb --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/Channel.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ShopBillingData.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ShopBillingData.xml new file mode 100644 index 0000000000..0a77587ba1 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ShopBillingData.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ChannelDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/ChannelDenormalizer.php index 503a789ee5..424f1e9704 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ChannelDenormalizer.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ChannelDenormalizer.php @@ -15,6 +15,7 @@ namespace Sylius\Bundle\ApiBundle\Serializer; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ChannelPriceHistoryConfigInterface; +use Sylius\Component\Core\Model\ShopBillingDataInterface; use Sylius\Component\Resource\Factory\FactoryInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; @@ -28,8 +29,14 @@ final class ChannelDenormalizer implements ContextAwareDenormalizerInterface, De private const ALREADY_CALLED = 'sylius_channel_denormalizer_already_called'; - public function __construct(private FactoryInterface $channelPriceHistoryConfigFactory) - { + /** + * @param FactoryInterface $channelPriceHistoryConfigFactory + * @param FactoryInterface $shopBillingDataFactory + */ + public function __construct( + private FactoryInterface $channelPriceHistoryConfigFactory, + private FactoryInterface $shopBillingDataFactory, + ) { } public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool @@ -54,6 +61,12 @@ final class ChannelDenormalizer implements ContextAwareDenormalizerInterface, De $channel->setChannelPriceHistoryConfig($channelPriceHistoryConfig); } + if (null === $channel->getShopBillingData()) { + /** @var ShopBillingDataInterface $shopBillingData */ + $shopBillingData = $this->shopBillingDataFactory->createNew(); + $channel->setShopBillingData($shopBillingData); + } + return $channel; } } diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/TranslatableDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/TranslatableDenormalizer.php new file mode 100644 index 0000000000..5bde6a9038 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Serializer/TranslatableDenormalizer.php @@ -0,0 +1,71 @@ +localeProvider->getDefaultLocaleCode(); + + if (!$this->hasDefaultTranslation($data['translations'] ?? [], $defaultLocaleCode)) { + $data['translations'][$defaultLocaleCode] = [ + 'locale' => $defaultLocaleCode, + ]; + } + + return $this->denormalizer->denormalize($data, $type, $format, $context); + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return + Request::METHOD_POST === ($context[ContextKeys::HTTP_REQUEST_METHOD_TYPE] ?? null) && + !isset($context[self::getAlreadyCalledKey($type)]) && + is_a($type, TranslatableInterface::class, true) + ; + } + + private static function getAlreadyCalledKey(string $class): string + { + return sprintf(self::ALREADY_CALLED, $class); + } + + private function hasDefaultTranslation(array $translations, string $defaultLocale): bool + { + return + isset($translations[$defaultLocale]['locale']) && + $defaultLocale === $translations[$defaultLocale]['locale'] + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/TranslatableLocaleKeyDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/TranslatableLocaleKeyDenormalizer.php new file mode 100644 index 0000000000..b9fbae3dac --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Serializer/TranslatableLocaleKeyDenormalizer.php @@ -0,0 +1,52 @@ + }> $data + */ + public function denormalize(mixed $data, string $type, string $format = null, array $context = []) + { + $context[self::ALREADY_CALLED] = true; + + if (array_key_exists('translations', $data)) { + foreach ($data['translations'] as $key => &$translation) { + $translation['locale'] = $key; + } + } + + return $this->denormalizer->denormalize($data, $type, $format, $context); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/.env.test_with_swiftmailer b/src/Sylius/Bundle/ApiBundle/Tests/Application/.env.test_with_swiftmailer deleted file mode 100644 index 26a5ec11b1..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/.env.test_with_swiftmailer +++ /dev/null @@ -1 +0,0 @@ -APP_ENV=test_with_swiftmailer diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/bundles.php b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/bundles.php index e13a5b1da0..6aa2bdbb12 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/bundles.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/bundles.php @@ -13,7 +13,6 @@ declare(strict_types=1); return [ Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], - Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['test_with_swiftmailer' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], @@ -54,9 +53,9 @@ return [ Sylius\Bundle\PayumBundle\SyliusPayumBundle::class => ['all' => true], ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], Sylius\Bundle\ApiBundle\SyliusApiBundle::class => ['all' => true], - Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true, 'test_with_swiftmailer' => true], - Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true, 'test_with_swiftmailer' => true], - Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true, 'test_with_swiftmailer' => true], + Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true], + Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true], + Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true], Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], SyliusLabs\DoctrineMigrationsExtraBundle\SyliusLabsDoctrineMigrationsExtraBundle::class => ['all' => true], SyliusLabs\Polyfill\Symfony\Security\Bundle\SyliusLabsPolyfillSymfonySecurityBundle::class => ['all' => true], diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/packages/test_with_swiftmailer/filesystem.yaml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/packages/test_with_swiftmailer/filesystem.yaml deleted file mode 100644 index 1828fd69a2..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/packages/test_with_swiftmailer/filesystem.yaml +++ /dev/null @@ -1,4 +0,0 @@ -services: - test.filesystem.public: - alias: filesystem - public: true diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/packages/test_with_swiftmailer/swiftmailer.yaml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/packages/test_with_swiftmailer/swiftmailer.yaml deleted file mode 100644 index c438f4b256..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/packages/test_with_swiftmailer/swiftmailer.yaml +++ /dev/null @@ -1,6 +0,0 @@ -swiftmailer: - disable_delivery: true - logging: true - spool: - type: file - path: "%kernel.cache_dir%/spool" diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/packages/test_with_swiftmailer/sylius_mailer.yaml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/packages/test_with_swiftmailer/sylius_mailer.yaml deleted file mode 100644 index e2310f20bf..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/packages/test_with_swiftmailer/sylius_mailer.yaml +++ /dev/null @@ -1,2 +0,0 @@ -sylius_mailer: - sender_adapter: sylius.email_sender.adapter.swiftmailer diff --git a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendAccountRegistrationEmailHandlerTest.php b/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendAccountRegistrationEmailHandlerTest.php index cf1e1472f3..6117d0268f 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendAccountRegistrationEmailHandlerTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendAccountRegistrationEmailHandlerTest.php @@ -19,25 +19,18 @@ use Sylius\Bundle\ApiBundle\Command\Account\SendAccountRegistrationEmail; use Sylius\Bundle\ApiBundle\CommandHandler\Account\SendAccountRegistrationEmailHandler; use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; use Sylius\Component\Core\Model\ChannelInterface; -use Sylius\Component\Core\Test\SwiftmailerAssertionTrait; use Sylius\Component\User\Model\UserInterface; use Sylius\Component\User\Repository\UserRepositoryInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Component\Filesystem\Filesystem; use Symfony\Contracts\Translation\TranslatorInterface; final class SendAccountRegistrationEmailHandlerTest extends KernelTestCase { use ProphecyTrait; - use SwiftmailerAssertionTrait; /** @test */ public function it_sends_account_registration_email(): void { - if ($this->isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment without swiftmailer'); - } - $container = self::getContainer(); /** @var TranslatorInterface $translator */ @@ -82,71 +75,4 @@ final class SendAccountRegistrationEmailHandlerTest extends KernelTestCase self::assertEmailAddressContains($email, 'To', 'user@example.com'); self::assertEmailHtmlBodyContains($email, $translator->trans('sylius.email.user_registration.start_shopping', [], null, 'en_US')); } - - /** @test */ - public function it_sends_account_registration_email_with_swiftmailer(): void - { - if (!$this->isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment with swiftmailer'); - } - - $container = self::getContainer(); - - self::setSpoolDirectory($container->getParameter('kernel.cache_dir') . '/spool'); - - /** @var Filesystem $filesystem */ - $filesystem = $container->get('test.filesystem.public'); - - /** @var TranslatorInterface $translator */ - $translator = $container->get('translator'); - - $filesystem->remove(self::getSpoolDirectory()); - - $emailSender = $container->get('sylius.email_sender'); - - /** @var ChannelRepositoryInterface|ObjectProphecy $channelRepository */ - $channelRepository = $this->prophesize(ChannelRepositoryInterface::class); - /** @var UserRepositoryInterface|ObjectProphecy $userRepository */ - $userRepository = $this->prophesize(UserRepositoryInterface::class); - /** @var ChannelInterface|ObjectProphecy $channel */ - $channel = $this->prophesize(ChannelInterface::class); - /** @var UserInterface|ObjectProphecy $user */ - $user = $this->prophesize(UserInterface::class); - - $channel->isAccountVerificationRequired()->willReturn(false); - $channel->getHostname()->willReturn('example.com'); - - $user->getUsername()->willReturn('username'); - $user->getEmailVerificationToken()->willReturn('token'); - - $channelRepository->findOneByCode('CHANNEL_CODE')->willReturn($channel->reveal()); - $userRepository->findOneByEmail('user@example.com')->willReturn($user->reveal()); - - $sendAccountRegistrationEmailHandler = new SendAccountRegistrationEmailHandler( - $userRepository->reveal(), - $channelRepository->reveal(), - $emailSender, - ); - - $sendAccountRegistrationEmailHandler( - new SendAccountRegistrationEmail( - 'user@example.com', - 'en_US', - 'CHANNEL_CODE', - ), - ); - - self::assertSpooledMessagesCountWithRecipient(1, 'user@example.com'); - self::assertSpooledMessageWithContentHasRecipient( - $translator->trans('sylius.email.user_registration.start_shopping', [], null, 'en_US'), - 'user@example.com', - ); - } - - private function isItSwiftmailerTestEnv(): bool - { - $env = self::getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } } diff --git a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendAccountVerificationEmailHandlerTest.php b/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendAccountVerificationEmailHandlerTest.php index c191f32f1c..1b599a7a0e 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendAccountVerificationEmailHandlerTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendAccountVerificationEmailHandlerTest.php @@ -33,10 +33,6 @@ final class SendAccountVerificationEmailHandlerTest extends KernelTestCase /** @test */ public function it_sends_account_verification_token_email(): void { - if ($this->isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment without swiftmailer'); - } - $container = self::getContainer(); /** @var TranslatorInterface $translator */ @@ -81,11 +77,4 @@ final class SendAccountVerificationEmailHandlerTest extends KernelTestCase $translator->trans('sylius.email.verification_token.message', [], null, 'en_US'), ); } - - private function isItSwiftmailerTestEnv(): bool - { - $env = self::getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } } diff --git a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendContactRequestHandlerTest.php b/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendContactRequestHandlerTest.php index f63e347593..8f71931a63 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendContactRequestHandlerTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendContactRequestHandlerTest.php @@ -31,10 +31,6 @@ final class SendContactRequestHandlerTest extends KernelTestCase /** @test */ public function it_sends_contact_request(): void { - if ($this->isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment without swiftmailer'); - } - $container = self::getContainer(); /** @var TranslatorInterface $translator */ @@ -68,11 +64,4 @@ final class SendContactRequestHandlerTest extends KernelTestCase self::assertEmailAddressContains($email, 'To', 'shop@example.com'); self::assertEmailHtmlBodyContains($email, $translator->trans('sylius.email.contact_request.content', [], null, 'en_US')); } - - private function isItSwiftmailerTestEnv(): bool - { - $env = self::getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } } diff --git a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendOrderConfirmationEmailHandlerTest.php b/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendOrderConfirmationEmailHandlerTest.php index 224454c019..cd391103e7 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendOrderConfirmationEmailHandlerTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendOrderConfirmationEmailHandlerTest.php @@ -32,10 +32,6 @@ final class SendOrderConfirmationEmailHandlerTest extends KernelTestCase /** @test */ public function it_sends_order_confirmation_email(): void { - if ($this->isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment without swiftmailer'); - } - $container = self::bootKernel()->getContainer(); $emailSender = $container->get('sylius.email_sender'); @@ -70,11 +66,4 @@ final class SendOrderConfirmationEmailHandlerTest extends KernelTestCase $this->assertEmailAddressContains($email, 'To', 'johnny.bravo@email.com'); $this->assertEmailHtmlBodyContains($email, '#000001'); } - - private function isItSwiftmailerTestEnv(): bool - { - $env = $this->getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } } diff --git a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendResetPasswordEmailHandlerTest.php b/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendResetPasswordEmailHandlerTest.php index 5cc8c19925..94245e9d20 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendResetPasswordEmailHandlerTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendResetPasswordEmailHandlerTest.php @@ -33,10 +33,6 @@ final class SendResetPasswordEmailHandlerTest extends KernelTestCase /** @test */ public function it_sends_password_reset_token_email(): void { - if ($this->isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment without swiftmailer'); - } - $container = self::getContainer(); /** @var TranslatorInterface $translator */ @@ -79,11 +75,4 @@ final class SendResetPasswordEmailHandlerTest extends KernelTestCase $translator->trans('sylius.email.password_reset.to_reset_your_password_token', [], null, 'en_US'), ); } - - private function isItSwiftmailerTestEnv(): bool - { - $env = self::getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } } diff --git a/src/Sylius/Bundle/ApiBundle/Tests/MessageHandler/Admin/SendResetPasswordEmailHandlerTest.php b/src/Sylius/Bundle/ApiBundle/Tests/MessageHandler/Admin/SendResetPasswordEmailHandlerTest.php index be39846a29..bf2279f62a 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/MessageHandler/Admin/SendResetPasswordEmailHandlerTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/MessageHandler/Admin/SendResetPasswordEmailHandlerTest.php @@ -29,10 +29,6 @@ final class SendResetPasswordEmailHandlerTest extends KernelTestCase /** @test */ public function it_sends_password_reset_token_email(): void { - if ($this->isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment without swiftmailer'); - } - $container = self::getContainer(); /** @var TranslatorInterface $translator */ @@ -66,11 +62,4 @@ final class SendResetPasswordEmailHandlerTest extends KernelTestCase $translator->trans('sylius.email.admin_password_reset.to_reset_your_password_token', [], null, 'en_US'), ); } - - private function isItSwiftmailerTestEnv(): bool - { - $env = self::getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } } diff --git a/src/Sylius/Bundle/ApiBundle/disabledSpec/DataPersister/ChannelDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/disabledSpec/DataPersister/ChannelDataPersisterSpec.php new file mode 100644 index 0000000000..e0750d5f78 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/disabledSpec/DataPersister/ChannelDataPersisterSpec.php @@ -0,0 +1,68 @@ +beConstructedWith($decoratedDataPersister, $channelDeletionChecker); + } + + function it_supports_only_channel_entity(ChannelInterface $channel, \stdClass $object): void + { + $this->supports($channel)->shouldReturn(true); + $this->supports($object)->shouldReturn(false); + } + + function it_persists_channel( + ContextAwareDataPersisterInterface $decoratedDataPersister, + ChannelInterface $channel, + ): void { + $decoratedDataPersister->persist($channel, [])->willReturn($channel); + + $this->persist($channel, [])->shouldReturn($channel); + } + + function it_throws_an_exception_if_channel_is_not_deletable( + ChannelDeletionCheckerInterface $channelDeletionChecker, + ChannelInterface $channel, + ): void { + $channelDeletionChecker->isDeletable($channel)->willReturn(false); + + $this + ->shouldThrow(ChannelCannotBeRemoved::class) + ->during('remove', [$channel]) + ; + } + + function it_removes_channel( + ContextAwareDataPersisterInterface $decoratedDataPersister, + ChannelDeletionCheckerInterface $channelDeletionChecker, + ChannelInterface $channel, + ): void { + $channelDeletionChecker->isDeletable($channel)->willReturn(true); + $decoratedDataPersister->remove($channel, [])->willReturn(null); + + $this->remove($channel, [])->shouldReturn(null); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/disabledSpec/DataPersister/TranslatableDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/disabledSpec/DataPersister/TranslatableDataPersisterSpec.php new file mode 100644 index 0000000000..2c903d3b4d --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/disabledSpec/DataPersister/TranslatableDataPersisterSpec.php @@ -0,0 +1,78 @@ +beConstructedWith($localeProvider); + } + + function it_is_a_context_aware_data_persister(): void + { + $this->shouldImplement(ContextAwareDataPersisterInterface::class); + } + + function it_is_a_resumable_data_persister(): void + { + $this->shouldImplement(ResumableDataPersisterInterface::class); + } + + function it_supports_only_translatable(TranslatableInterface $translatable): void + { + $this->supports(new \stdClass())->shouldReturn(false); + $this->supports($translatable)->shouldReturn(true); + } + + function it_does_nothing_if_there_is_a_translation_in_default_locale( + TranslationLocaleProviderInterface $localeProvider, + TranslatableInterface $translatable, + TranslatableInterface $translation, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en_US'); + $translatable->getTranslations()->willReturn(new ArrayCollection(['en_US' => $translation])); + + $this->persist($translatable)->shouldReturn($translatable); + } + + function it_throws_an_exception_if_there_is_no_translation_in_default_locale( + TranslationLocaleProviderInterface $localeProvider, + TranslatableInterface $translatable, + TranslatableInterface $translation, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en_US'); + $translatable->getTranslations()->willReturn(new ArrayCollection(['de_DE' => $translation])); + + $this->shouldThrow(TranslationInDefaultLocaleCannotBeRemoved::class)->during('persist', [$translatable]); + } + + function it_does_nothing_during_removing_object(TranslatableInterface $translatable): void + { + $this->remove($translatable)->shouldReturn($translatable); + } + + function it_is_resumable(): void + { + $this->resumable()->shouldReturn(true); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/disabledSpec/Serializer/ChannelDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/disabledSpec/Serializer/ChannelDenormalizerSpec.php index 79f1e9b7af..64770e448e 100644 --- a/src/Sylius/Bundle/ApiBundle/disabledSpec/Serializer/ChannelDenormalizerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/disabledSpec/Serializer/ChannelDenormalizerSpec.php @@ -17,6 +17,7 @@ use PhpSpec\ObjectBehavior; use Prophecy\Argument; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ChannelPriceHistoryConfigInterface; +use Sylius\Component\Core\Model\ShopBillingDataInterface; use Sylius\Component\Resource\Factory\FactoryInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -24,9 +25,9 @@ final class ChannelDenormalizerSpec extends ObjectBehavior { private const ALREADY_CALLED = 'sylius_channel_denormalizer_already_called'; - function let(FactoryInterface $configFactory): void + function let(FactoryInterface $configFactory, FactoryInterface $shopBillingDataFactory): void { - $this->beConstructedWith($configFactory); + $this->beConstructedWith($configFactory, $shopBillingDataFactory); } function it_does_not_support_denormalization_when_the_denormalizer_has_already_been_called(): void @@ -54,15 +55,17 @@ final class ChannelDenormalizerSpec extends ObjectBehavior $this->shouldThrow(\InvalidArgumentException::class)->during('denormalize', [[], 'string']); } - function it_returns_channel_as_is_when_channel_price_history_config_has_already_been_set( + function it_returns_channel_as_is_when_shop_billing_data_and_channel_price_history_config_has_already_been_set( DenormalizerInterface $denormalizer, FactoryInterface $configFactory, + ShopBillingDataInterface $shopBillingData, ChannelPriceHistoryConfigInterface $config, ChannelInterface $channel, ): void { $this->setDenormalizer($denormalizer); $channel->getChannelPriceHistoryConfig()->willReturn($config); + $channel->getShopBillingData()->willReturn($shopBillingData); $channel->setChannelPriceHistoryConfig(Argument::any())->shouldNotBeCalled(); $configFactory->createNew()->shouldNotBeCalled(); @@ -75,12 +78,14 @@ final class ChannelDenormalizerSpec extends ObjectBehavior function it_adds_a_new_channel_price_history_config_when_channel_has_none( DenormalizerInterface $denormalizer, FactoryInterface $configFactory, + ShopBillingDataInterface $shopBillingData, ChannelPriceHistoryConfigInterface $config, ChannelInterface $channel, ): void { $this->setDenormalizer($denormalizer); $channel->getChannelPriceHistoryConfig()->willReturn(null); + $channel->getShopBillingData()->willReturn($shopBillingData); $configFactory->createNew()->willReturn($config); $channel->setChannelPriceHistoryConfig($config)->shouldBeCalled(); @@ -89,4 +94,24 @@ final class ChannelDenormalizerSpec extends ObjectBehavior $this->denormalize([], ChannelInterface::class)->shouldReturn($channel); } + + function it_adds_a_new_shop_billing_data_when_channel_has_none( + DenormalizerInterface $denormalizer, + FactoryInterface $shopBillingDataFactory, + ShopBillingDataInterface $shopBillingData, + ChannelPriceHistoryConfigInterface $config, + ChannelInterface $channel, + ): void { + $this->setDenormalizer($denormalizer); + + $channel->getChannelPriceHistoryConfig()->willReturn($config); + $channel->getShopBillingData()->willReturn(null); + + $shopBillingDataFactory->createNew()->willReturn($shopBillingData); + $channel->setShopBillingData($shopBillingData)->shouldBeCalled(); + + $denormalizer->denormalize([], ChannelInterface::class, null, [self::ALREADY_CALLED => true])->willReturn($channel); + + $this->denormalize([], ChannelInterface::class)->shouldReturn($channel); + } } diff --git a/src/Sylius/Bundle/ApiBundle/disabledSpec/Serializer/ProductAttributeValueDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/disabledSpec/Serializer/ProductAttributeValueDenormalizerSpec.php index 8407e4a0be..21bd22e96e 100644 --- a/src/Sylius/Bundle/ApiBundle/disabledSpec/Serializer/ProductAttributeValueDenormalizerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/disabledSpec/Serializer/ProductAttributeValueDenormalizerSpec.php @@ -88,7 +88,7 @@ final class ProductAttributeValueDenormalizerSpec extends ObjectBehavior ['attribute' => '/attributes/material', 'value' => 'ceramic'], ProductAttributeValueInterface::class, null, - [self::ALREADY_CALLED => true] + [self::ALREADY_CALLED => true], ) ->shouldBeCalled() ; diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/TranslatableDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/TranslatableDenormalizerSpec.php new file mode 100644 index 0000000000..6da72ab875 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/TranslatableDenormalizerSpec.php @@ -0,0 +1,186 @@ +beConstructedWith($localeProvider); + + $this->setDenormalizer($denormalizer); + } + + function it_only_supports_translatable_resource(): void + { + $this->supportsDenormalization([], TranslatableInterface::class, null, [ + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'PUT', + ])->shouldReturn(false); + + $this->supportsDenormalization([], TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldReturn(false); + + $this->supportsDenormalization([], \stdClass::class, null, [ + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldReturn(false); + } + + function it_does_nothing_when_data_contains_a_translation_in_default_locale( + DenormalizerInterface $denormalizer, + TranslationLocaleProviderInterface $localeProvider, + ): void { + $data = ['translations' => ['en' => ['locale' => 'en']]]; + + $localeProvider->getDefaultLocaleCode()->willReturn('en'); + + $denormalizer->denormalize($data, TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldBeCalled()->willReturn($data); + + $this + ->denormalize($data, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST']) + ->shouldReturn($data) + ; + } + + function it_adds_default_translation_when_no_translations_passed_in_data( + DenormalizerInterface $denormalizer, + TranslationLocaleProviderInterface $localeProvider, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en'); + + $updatedData = ['translations' => ['en' => ['locale' => 'en']]]; + + $denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldBeCalled()->willReturn($updatedData); + + $this + ->denormalize([], TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST']) + ->shouldReturn($updatedData) + ; + } + + function it_adds_default_translation_when_no_translation_passed_for_default_locale_in_data( + DenormalizerInterface $denormalizer, + TranslationLocaleProviderInterface $localeProvider, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en'); + + $originalData = ['translations' => ['en' => []]]; + $updatedData = ['translations' => ['en' => ['locale' => 'en']]]; + + $denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldBeCalled()->willReturn($updatedData); + + $this + ->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST']) + ->shouldReturn($updatedData) + ; + } + + function it_adds_default_translation_when_passed_default_translation_has_empty_locale( + DenormalizerInterface $denormalizer, + TranslationLocaleProviderInterface $localeProvider, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en'); + + $originalData = ['translations' => ['en' => ['locale' => '']]]; + $updatedData = ['translations' => ['en' => ['locale' => 'en']]]; + + $denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldBeCalled()->willReturn($updatedData); + + $this + ->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST']) + ->shouldReturn($updatedData) + ; + } + + function it_adds_default_translation_when_passed_default_translation_has_null_locale( + DenormalizerInterface $denormalizer, + TranslationLocaleProviderInterface $localeProvider, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en'); + + $originalData = ['translations' => ['en' => ['locale' => null]]]; + $updatedData = ['translations' => ['en' => ['locale' => 'en']]]; + + $denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldBeCalled()->willReturn($updatedData); + + $this + ->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST']) + ->shouldReturn($updatedData) + ; + } + + function it_adds_default_translation_when_passed_default_translation_has_mismatched_locale( + DenormalizerInterface $denormalizer, + TranslationLocaleProviderInterface $localeProvider, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en'); + + $originalData = ['translations' => ['en' => ['locale' => 'fr']]]; + $updatedData = ['translations' => ['en' => ['locale' => 'en']]]; + + $denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldBeCalled()->willReturn($updatedData); + + $this + ->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST']) + ->shouldReturn($updatedData) + ; + } + + function it_adds_default_translation_when_no_translation_in_default_locale_passed_in_data( + DenormalizerInterface $denormalizer, + TranslationLocaleProviderInterface $localeProvider, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en'); + + $originalData = ['translations' => ['pl' => ['locale' => 'pl']]]; + $updatedData = ['translations' => ['en' => ['locale' => 'en'], 'pl' => ['locale' => 'pl']]]; + + $denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldBeCalled()->willReturn($updatedData); + + $this + ->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST']) + ->shouldReturn($updatedData) + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/TranslatableLocaleKeyDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/TranslatableLocaleKeyDenormalizerSpec.php new file mode 100644 index 0000000000..f41f132e7d --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/TranslatableLocaleKeyDenormalizerSpec.php @@ -0,0 +1,79 @@ +supportsDenormalization([], TranslatableInterface::class, context: [self::ALREADY_CALLED => true]) + ->shouldReturn(false) + ; + } + + function it_does_not_support_denormalization_when_data_is_not_an_array(): void + { + $this->supportsDenormalization('string', TranslatableInterface::class)->shouldReturn(false); + } + + function it_does_not_support_denormalization_when_type_is_not_a_translatable(): void + { + $this->supportsDenormalization([], 'string')->shouldReturn(false); + } + + function it_does_nothing_if_there_is_no_translation_key( + DenormalizerInterface $denormalizer, + ): void { + $this->setDenormalizer($denormalizer); + + $this->denormalize([], TranslatableInterface::class); + + $denormalizer->denormalize([], TranslatableInterface::class, null, [self::ALREADY_CALLED => true])->shouldHaveBeenCalledOnce(); + } + + function it_changes_keys_of_translations_to_locale( + DenormalizerInterface $denormalizer, + ): void { + $this->setDenormalizer($denormalizer); + + $this->denormalize( + [ + 'translations' => [ + 'en_US' => ['locale' => 'en_US'], + 'de_DE' => ['locale' => 'locale'], + ], + ], + TranslatableInterface::class, + ); + + $denormalizer->denormalize( + [ + 'translations' => [ + 'en_US' => ['locale' => 'en_US'], + 'de_DE' => ['locale' => 'de_DE'], + ], + ], + TranslatableInterface::class, + null, + [self::ALREADY_CALLED => true], + )->shouldHaveBeenCalledOnce(); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/ShopUserResetPasswordTokenExistsValidatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/ShopUserResetPasswordTokenExistsValidatorSpec.php index 6f42fbe533..fc15e08e74 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/ShopUserResetPasswordTokenExistsValidatorSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/ShopUserResetPasswordTokenExistsValidatorSpec.php @@ -41,7 +41,7 @@ final class ShopUserResetPasswordTokenExistsValidatorSpec extends ObjectBehavior ; } - function it_throws_an_exception_if_constraint_is_not_a_resetPasswordTokenExists_constraint(): void + function it_throws_an_exception_if_constraint_is_not_a_reset_password_token_exists_constraint(): void { $this ->shouldThrow(\InvalidArgumentException::class) @@ -50,7 +50,7 @@ final class ShopUserResetPasswordTokenExistsValidatorSpec extends ObjectBehavior ; } - function it_does_not_add_violation_if_user_exist( + function it_does_not_add_violation_if_user_exists( UserRepositoryInterface $userRepository, ExecutionContextInterface $executionContext, UserInterface $user, diff --git a/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/ShopUserResetPasswordTokenNotExpiredValidatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/ShopUserResetPasswordTokenNotExpiredValidatorSpec.php index 67c109fe2b..1b104f7123 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/ShopUserResetPasswordTokenNotExpiredValidatorSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/ShopUserResetPasswordTokenNotExpiredValidatorSpec.php @@ -42,7 +42,7 @@ final class ShopUserResetPasswordTokenNotExpiredValidatorSpec extends ObjectBeha ; } - function it_throws_an_exception_if_constraint_is_not_a_resetPasswordTokenExpires_constraint(): void + function it_throws_an_exception_if_constraint_is_not_a_reset_password_token_not_expired_constraint(): void { $this ->shouldThrow(\InvalidArgumentException::class) diff --git a/src/Sylius/Bundle/AttributeBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/AttributeBundle/DependencyInjection/Configuration.php index b3726e4458..9ca1b94928 100644 --- a/src/Sylius/Bundle/AttributeBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/AttributeBundle/DependencyInjection/Configuration.php @@ -13,6 +13,9 @@ declare(strict_types=1); namespace Sylius\Bundle\AttributeBundle\DependencyInjection; +use Sylius\Bundle\AttributeBundle\Doctrine\ORM\AttributeRepository; +use Sylius\Bundle\AttributeBundle\Doctrine\ORM\AttributeTranslationRepository; +use Sylius\Bundle\AttributeBundle\Doctrine\ORM\AttributeValueRepository; use Sylius\Bundle\ResourceBundle\Controller\ResourceController; use Sylius\Bundle\ResourceBundle\SyliusResourceBundle; use Sylius\Component\Attribute\Model\Attribute; @@ -64,7 +67,7 @@ final class Configuration implements ConfigurationInterface ->scalarNode('model')->defaultValue(Attribute::class)->cannotBeEmpty()->end() ->scalarNode('interface')->defaultValue(AttributeInterface::class)->cannotBeEmpty()->end() ->scalarNode('controller')->cannotBeEmpty()->end() - ->scalarNode('repository')->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(AttributeRepository::class)->cannotBeEmpty()->end() ->scalarNode('factory')->defaultValue(TranslatableFactory::class)->end() ->scalarNode('form')->cannotBeEmpty()->end() ->end() @@ -79,7 +82,7 @@ final class Configuration implements ConfigurationInterface ->scalarNode('model')->defaultValue(AttributeTranslation::class)->cannotBeEmpty()->end() ->scalarNode('interface')->defaultValue(AttributeTranslationInterface::class)->cannotBeEmpty()->end() ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() - ->scalarNode('repository')->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(AttributeTranslationRepository::class)->cannotBeEmpty()->end() ->scalarNode('factory')->defaultValue(Factory::class)->end() ->scalarNode('form')->cannotBeEmpty()->end() ->end() @@ -99,7 +102,7 @@ final class Configuration implements ConfigurationInterface ->scalarNode('model')->isRequired()->cannotBeEmpty()->end() ->scalarNode('interface')->isRequired()->cannotBeEmpty()->end() ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() - ->scalarNode('repository')->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(AttributeValueRepository::class)->cannotBeEmpty()->end() ->scalarNode('factory')->defaultValue(Factory::class)->end() ->scalarNode('form')->cannotBeEmpty()->end() ->end() diff --git a/src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/AttributeRepository.php b/src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/AttributeRepository.php new file mode 100644 index 0000000000..777acea5f9 --- /dev/null +++ b/src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/AttributeRepository.php @@ -0,0 +1,25 @@ + + */ +class AttributeRepository extends EntityRepository implements AttributeRepositoryInterface +{ +} diff --git a/src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/AttributeTranslationRepository.php b/src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/AttributeTranslationRepository.php new file mode 100644 index 0000000000..2e5a784daa --- /dev/null +++ b/src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/AttributeTranslationRepository.php @@ -0,0 +1,25 @@ + + */ +class AttributeTranslationRepository extends EntityRepository implements AttributeTranslationRepositoryInterface +{ +} diff --git a/src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/AttributeValueRepository.php b/src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/AttributeValueRepository.php new file mode 100644 index 0000000000..9107746446 --- /dev/null +++ b/src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/AttributeValueRepository.php @@ -0,0 +1,25 @@ + + */ +class AttributeValueRepository extends EntityRepository implements AttributeValueRepositoryInterface +{ +} diff --git a/src/Sylius/Bundle/AttributeBundle/Resources/config/validation/AttributeTranslation.xml b/src/Sylius/Bundle/AttributeBundle/Resources/config/validation/AttributeTranslation.xml index 6587628f32..0bdb915ea8 100644 --- a/src/Sylius/Bundle/AttributeBundle/Resources/config/validation/AttributeTranslation.xml +++ b/src/Sylius/Bundle/AttributeBundle/Resources/config/validation/AttributeTranslation.xml @@ -26,5 +26,15 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/AttributeBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/AttributeBundle/Resources/translations/validators.en.yml index 2ffe830ae3..045b0fdd62 100644 --- a/src/Sylius/Bundle/AttributeBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/AttributeBundle/Resources/translations/validators.en.yml @@ -24,3 +24,7 @@ sylius: not_blank: Please select attribute. value: not_blank: Please enter attribute value. + translation: + locale: + not_blank: Please enter the locale. + invalid: This value is not a valid locale. diff --git a/src/Sylius/Bundle/ChannelBundle/Doctrine/ORM/ChannelRepository.php b/src/Sylius/Bundle/ChannelBundle/Doctrine/ORM/ChannelRepository.php index ecb5f7493f..6b27728119 100644 --- a/src/Sylius/Bundle/ChannelBundle/Doctrine/ORM/ChannelRepository.php +++ b/src/Sylius/Bundle/ChannelBundle/Doctrine/ORM/ChannelRepository.php @@ -56,6 +56,17 @@ class ChannelRepository extends EntityRepository implements ChannelRepositoryInt ; } + /** + * @return ChannelInterface[] + */ + public function findEnabled(): iterable + { + /** @var ChannelInterface[] $enabledChannels */ + $enabledChannels = $this->findBy(['enabled' => true]); + + return $enabledChannels; + } + public function countAll(): int { return $this->count([]); diff --git a/src/Sylius/Bundle/ChannelBundle/Resources/config/services.xml b/src/Sylius/Bundle/ChannelBundle/Resources/config/services.xml index 5c114a2502..cc0fdf70d8 100644 --- a/src/Sylius/Bundle/ChannelBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/ChannelBundle/Resources/config/services.xml @@ -69,5 +69,10 @@ false + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Checkout/CheckoutRedirectListener.php b/src/Sylius/Bundle/CoreBundle/Checkout/CheckoutRedirectListener.php index b944adb4e3..afbf13f2e0 100644 --- a/src/Sylius/Bundle/CoreBundle/Checkout/CheckoutRedirectListener.php +++ b/src/Sylius/Bundle/CoreBundle/Checkout/CheckoutRedirectListener.php @@ -32,13 +32,19 @@ final class CheckoutRedirectListener public function handleCheckoutRedirect(ResourceControllerEvent $resourceControllerEvent): void { $request = $this->requestStack->getCurrentRequest(); - if (!$this->requestMatcher->matches($request) || isset($request->attributes->get('_sylius')['redirect'])) { + if ( + null === $request || + !$this->requestMatcher->matches($request) || + isset($request->attributes->get('_sylius', [])['redirect']) + ) { return; } $order = $resourceControllerEvent->getSubject(); Assert::isInstanceOf($order, OrderInterface::class); - $resourceControllerEvent->setResponse(new RedirectResponse($this->checkoutStateUrlGenerator->generateForOrderCheckoutState($order))); + $resourceControllerEvent->setResponse( + new RedirectResponse($this->checkoutStateUrlGenerator->generateForOrderCheckoutState($order)), + ); } } diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AddressRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AddressRepository.php index bcca8d8b37..590ec222cb 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AddressRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AddressRepository.php @@ -13,17 +13,12 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Doctrine\ORM; -use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Bundle\AddressingBundle\Doctrine\ORM\AddressRepository as BaseAddressRepository; use Sylius\Component\Core\Model\AddressInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Repository\AddressRepositoryInterface; -/** - * @template T of AddressInterface - * - * @implements AddressRepositoryInterface - */ -class AddressRepository extends EntityRepository implements AddressRepositoryInterface +class AddressRepository extends BaseAddressRepository implements AddressRepositoryInterface { public function findByCustomer(CustomerInterface $customer): array { diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AttributeRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AttributeRepository.php index cc3c53917a..83b4ecde9d 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AttributeRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AttributeRepository.php @@ -15,14 +15,14 @@ namespace Sylius\Bundle\CoreBundle\Doctrine\ORM; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; -use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Bundle\AttributeBundle\Doctrine\ORM\AttributeRepository as BaseAttributeRepository; use Sylius\Component\Attribute\Model\AttributeInterface; use SyliusLabs\AssociationHydrator\AssociationHydrator; /** * @template T of AttributeInterface */ -class AttributeRepository extends EntityRepository +class AttributeRepository extends BaseAttributeRepository { protected AssociationHydrator $associationHydrator; diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/ChannelDeletionListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/ChannelDeletionListener.php index 742375b63c..9966020af5 100644 --- a/src/Sylius/Bundle/CoreBundle/EventListener/ChannelDeletionListener.php +++ b/src/Sylius/Bundle/CoreBundle/EventListener/ChannelDeletionListener.php @@ -14,13 +14,13 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\EventListener; use Sylius\Bundle\ResourceBundle\Event\ResourceControllerEvent; +use Sylius\Component\Channel\Checker\ChannelDeletionCheckerInterface; use Sylius\Component\Channel\Model\ChannelInterface; -use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; use Sylius\Component\Resource\Exception\UnexpectedTypeException; final class ChannelDeletionListener { - public function __construct(private ChannelRepositoryInterface $channelRepository) + public function __construct(private ChannelDeletionCheckerInterface $channelDeletionChecker) { } @@ -38,9 +38,7 @@ final class ChannelDeletionListener ); } - $results = $this->channelRepository->findBy(['enabled' => true]); - - if (!$results || (count($results) === 1 && current($results) === $channel)) { + if (!$this->channelDeletionChecker->isDeletable($channel)) { $event->stop('sylius.channel.delete_error'); } } diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/AssignOrderNumberListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/AssignOrderNumberListener.php new file mode 100644 index 0000000000..b462751b25 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/AssignOrderNumberListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderNumberAssigner->assignNumber($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/AssignOrderTokenListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/AssignOrderTokenListener.php new file mode 100644 index 0000000000..33216edd71 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/AssignOrderTokenListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderTokenAssigner->assignTokenValueIfNotSet($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelOrderPaymentListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelOrderPaymentListener.php new file mode 100644 index 0000000000..2fbb8e153c --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelOrderPaymentListener.php @@ -0,0 +1,36 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->compositeOrderStateMachine->apply($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_CANCEL); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelOrderShippingListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelOrderShippingListener.php new file mode 100644 index 0000000000..309bc30406 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelOrderShippingListener.php @@ -0,0 +1,36 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->compositeOrderStateMachine->apply($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_CANCEL); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelPaymentListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelPaymentListener.php new file mode 100644 index 0000000000..316b7c7037 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelPaymentListener.php @@ -0,0 +1,40 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $payments = $order->getPayments(); + + foreach ($payments as $payment) { + $this->compositeOrderStateMachine->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL); + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelShipmentListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelShipmentListener.php new file mode 100644 index 0000000000..6950c8a56e --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelShipmentListener.php @@ -0,0 +1,40 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $shipments = $order->getShipments(); + + foreach ($shipments as $shipment) { + $this->compositeOrderStateMachine->apply($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CANCEL); + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CreatePaymentListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CreatePaymentListener.php new file mode 100644 index 0000000000..636ba1b411 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CreatePaymentListener.php @@ -0,0 +1,40 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $payments = $order->getPayments(); + + foreach ($payments as $payment) { + $this->compositeStateMachine->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CREATE); + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CreateShipmentListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CreateShipmentListener.php new file mode 100644 index 0000000000..1ffb293459 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CreateShipmentListener.php @@ -0,0 +1,40 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $shipments = $order->getShipments(); + + foreach ($shipments as $shipment) { + $this->compositeStateMachine->apply($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CREATE); + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/DecrementPromotionUsagesListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/DecrementPromotionUsagesListener.php new file mode 100644 index 0000000000..2c945743b4 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/DecrementPromotionUsagesListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderPromotionsUsageModifier->decrement($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/GiveBackInventoryListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/GiveBackInventoryListener.php new file mode 100644 index 0000000000..96899a21a0 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/GiveBackInventoryListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderInventoryOperator->cancel($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/HoldInventoryListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/HoldInventoryListener.php new file mode 100644 index 0000000000..025b28c725 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/HoldInventoryListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderInventoryOperator->hold($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/IncrementPromotionUsagesListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/IncrementPromotionUsagesListener.php new file mode 100644 index 0000000000..0cf3c6af33 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/IncrementPromotionUsagesListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderPromotionsUsageModifier->increment($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/RequestOrderPaymentListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/RequestOrderPaymentListener.php new file mode 100644 index 0000000000..db44b11978 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/RequestOrderPaymentListener.php @@ -0,0 +1,36 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->compositeStateMachine->apply($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_REQUEST_PAYMENT); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/RequestOrderShippingListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/RequestOrderShippingListener.php new file mode 100644 index 0000000000..6f2939dc81 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/RequestOrderShippingListener.php @@ -0,0 +1,36 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->compositeStateMachine->apply($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_REQUEST_SHIPPING); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/SaveCustomerAddressesListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/SaveCustomerAddressesListener.php new file mode 100644 index 0000000000..6843f98612 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/SaveCustomerAddressesListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderAddressesSaver->saveAddresses($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/SetImmutableNamesListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/SetImmutableNamesListener.php new file mode 100644 index 0000000000..5d94c2122e --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/SetImmutableNamesListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderItemNamesSetter->__invoke($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20231103004216.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20231103004216.php new file mode 100644 index 0000000000..4be36ec16a --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20231103004216.php @@ -0,0 +1,41 @@ +addSql('CREATE UNIQUE INDEX UNIQ_88D5CC4D6B7BA4B6 ON sylius_admin_user (password_reset_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_88D5CC4DC4995C67 ON sylius_admin_user (email_verification_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_7C2B74806B7BA4B6 ON sylius_shop_user (password_reset_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_7C2B7480C4995C67 ON sylius_shop_user (email_verification_token)'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP INDEX UNIQ_88D5CC4D6B7BA4B6 ON sylius_admin_user'); + $this->addSql('DROP INDEX UNIQ_88D5CC4DC4995C67 ON sylius_admin_user'); + $this->addSql('DROP INDEX UNIQ_7C2B74806B7BA4B6 ON sylius_shop_user'); + $this->addSql('DROP INDEX UNIQ_7C2B7480C4995C67 ON sylius_shop_user'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/app/workflow/sylius_order.yaml b/src/Sylius/Bundle/CoreBundle/Resources/config/app/workflow/sylius_order.yaml new file mode 100644 index 0000000000..04e43688ac --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/app/workflow/sylius_order.yaml @@ -0,0 +1,25 @@ +framework: + workflows: + !php/const Sylius\Component\Order\OrderTransitions::GRAPH: + type: state_machine + marking_store: + type: method + property: state + supports: + - Sylius\Component\Core\Model\OrderInterface + initial_marking: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CART + places: + - !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CART + - !php/const Sylius\Component\Order\Model\OrderInterface::STATE_NEW + - !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CANCELLED + - !php/const Sylius\Component\Order\Model\OrderInterface::STATE_FULFILLED + transitions: + !php/const Sylius\Component\Order\OrderTransitions::TRANSITION_CREATE: + from: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CART + to: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_NEW + !php/const Sylius\Component\Order\OrderTransitions::TRANSITION_CANCEL: + from: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_NEW + to: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CANCELLED + !php/const Sylius\Component\Order\OrderTransitions::TRANSITION_FULFILL: + from: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_NEW + to: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_FULFILLED diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners.xml index 34d58f5f24..281e5cae54 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners.xml @@ -20,7 +20,7 @@ - + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order.xml new file mode 100644 index 0000000000..080b174b22 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order_checkout.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order_checkout.xml index d8b309bc1c..794a7f5ba3 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order_checkout.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order_checkout.xml @@ -13,8 +13,6 @@ - - diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderWorkflowTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderWorkflowTest.php new file mode 100644 index 0000000000..af27fb5639 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderWorkflowTest.php @@ -0,0 +1,80 @@ +createMock(RepositoryInterface::class); + $sequenceRepository + ->method('findOneBy') + ->willReturn(null) + ; + + self::getContainer()->set('sylius.repository.order_sequence', $sequenceRepository); + + $channel = $this->createMock(ChannelInterface::class); + $customer = $this->createMock(Customer::class); + + $order = new Order(); + $order->setChannel($channel); + $order->setCustomer($customer); + + $this->order = $order; + } + + /** + * @test + * + * @dataProvider availableTransitionsForOrder + */ + public function it_applies_all_available_transitions_for_order( + string $initialState, + string $transition, + string $expectedState, + ): void { + $stateMachine = $this->getStateMachine(); + $order = $this->order; + + $stateMachine->apply($order, OrderTransitions::GRAPH, $initialState); + $stateMachine->apply($order, OrderTransitions::GRAPH, $transition); + + $this->assertSame($expectedState, $order->getState()); + } + + public function availableTransitionsForOrder(): iterable + { + yield [OrderTransitions::TRANSITION_CREATE, OrderTransitions::TRANSITION_CANCEL, OrderInterface::STATE_CANCELLED]; + yield [OrderTransitions::TRANSITION_CREATE, OrderTransitions::TRANSITION_FULFILL, OrderInterface::STATE_FULFILLED]; + } + + private function getStateMachine(): StateMachineInterface + { + return self::getContainer()->get('sylius.state_machine.adapter.symfony_workflow'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Mailer/OrderEmailManagerTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Mailer/OrderEmailManagerTest.php index af9d469c87..70378b7a91 100644 --- a/src/Sylius/Bundle/CoreBundle/Tests/Mailer/OrderEmailManagerTest.php +++ b/src/Sylius/Bundle/CoreBundle/Tests/Mailer/OrderEmailManagerTest.php @@ -19,15 +19,12 @@ use Sylius\Bundle\CoreBundle\Mailer\OrderEmailManagerInterface; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; -use Sylius\Component\Core\Test\SwiftmailerAssertionTrait; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Component\Filesystem\Filesystem; use Symfony\Contracts\Translation\TranslatorInterface; final class OrderEmailManagerTest extends KernelTestCase { use ProphecyTrait; - use SwiftmailerAssertionTrait; private const RECIPIENT_EMAIL = 'test@example.com'; @@ -36,12 +33,8 @@ final class OrderEmailManagerTest extends KernelTestCase private const ORDER_NUMBER = '#000001'; /** @test */ - public function it_sends_order_confirmation_email_with_symfony_mailer_if_swift_mailer_is_not_present(): void + public function it_sends_order_confirmation_email(): void { - if ($this->isItSwiftmailerTestEnv()) { - $this->markTestSkipped('This test should be executed only outside of test_with_swiftmailer environment'); - } - $container = self::getContainer(); /** @var TranslatorInterface $translator */ @@ -78,59 +71,4 @@ final class OrderEmailManagerTest extends KernelTestCase preg_replace('/\s+/', ' ', strip_tags($email->getHtmlBody())), ); } - - /** @test */ - public function it_sends_order_confirmation_email_with_swift_mailer_by_default_if_is_present(): void - { - if (!$this->isItSwiftmailerTestEnv()) { - $this->markTestSkipped('This test should be executed only in test_with_swiftmailer environment'); - } - - $container = self::getContainer(); - - self::setSpoolDirectory($container->getParameter('kernel.cache_dir') . '/spool'); - - /** @var Filesystem $filesystem */ - $filesystem = $container->get('filesystem'); - - /** @var TranslatorInterface $translator */ - $translator = $container->get('translator'); - - $filesystem->remove(self::getSpoolDirectory()); - - $orderEmailManager = static::$kernel->getContainer()->get('sylius.mailer.order_email_manager'); - /** @var OrderInterface|ObjectProphecy $order */ - $order = $this->prophesize(OrderInterface::class); - /** @var CustomerInterface|ObjectProphecy $customer */ - $customer = $this->prophesize(CustomerInterface::class); - $customer->getEmail()->willReturn(self::RECIPIENT_EMAIL); - /** @var ChannelInterface|ObjectProphecy $channel */ - $channel = $this->prophesize(ChannelInterface::class); - - $order->getCustomer()->willReturn($customer->reveal()); - $order->getChannel()->willReturn($channel->reveal()); - $order->getLocaleCode()->willReturn(self::LOCALE_CODE); - $order->getNumber()->willReturn(self::ORDER_NUMBER); - $order->getTokenValue()->willReturn('ASFAFA4654AF'); - - $orderEmailManager->sendConfirmationEmail($order->reveal()); - - self::assertSpooledMessagesCountWithRecipient(1, self::RECIPIENT_EMAIL); - self::assertSpooledMessageWithContentHasRecipient( - sprintf( - '%s %s %s', - $translator->trans('sylius.email.order_confirmation.your_order_number', [], null, self::LOCALE_CODE), - self::ORDER_NUMBER, - $translator->trans('sylius.email.order_confirmation.has_been_successfully_placed', [], null, self::LOCALE_CODE), - ), - self::RECIPIENT_EMAIL, - ); - } - - private function isItSwiftmailerTestEnv(): bool - { - $env = self::getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } } diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/AssignOrderNumberListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/AssignOrderNumberListenerSpec.php new file mode 100644 index 0000000000..4ba629d86d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/AssignOrderNumberListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderNumberAssigner); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $subject): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new TransitionEvent($subject->getWrappedObject(), new Marking())]) + ; + } + + function it_assigns_order_number(OrderNumberAssignerInterface $orderNumberAssigner): void + { + $order = new Order(); + $event = new TransitionEvent($order, new Marking()); + + $orderNumberAssigner->assignNumber($order)->shouldBeCalled(); + + $this($event); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/AssignOrderTokenListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/AssignOrderTokenListenerSpec.php new file mode 100644 index 0000000000..2877134759 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/AssignOrderTokenListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderTokenAssigner); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $subject): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new TransitionEvent($subject->getWrappedObject(), new Marking())]) + ; + } + + function it_assigns_order_token(OrderTokenAssignerInterface $orderTokenAssigner): void + { + $order = new Order(); + $event = new TransitionEvent($order, new Marking()); + + $orderTokenAssigner->assignTokenValueIfNotSet($order)->shouldBeCalled(); + + $this($event); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelOrderPaymentListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelOrderPaymentListenerSpec.php new file mode 100644 index 0000000000..048efc8d0b --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelOrderPaymentListenerSpec.php @@ -0,0 +1,50 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_cancels_order_payment( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $compositeStateMachine + ->apply($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_CANCEL) + ->shouldBeCalled() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelOrderShippingListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelOrderShippingListenerSpec.php new file mode 100644 index 0000000000..c903deed1a --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelOrderShippingListenerSpec.php @@ -0,0 +1,50 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_cancels_order_shipping( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $compositeStateMachine + ->apply($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_CANCEL) + ->shouldBeCalled() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelPaymentListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelPaymentListenerSpec.php new file mode 100644 index 0000000000..b6b37b9e9d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelPaymentListenerSpec.php @@ -0,0 +1,60 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_cancels_payments( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + PaymentInterface $payment1, + PaymentInterface $payment2, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + $order->getPayments()->willReturn(new ArrayCollection([$payment1->getWrappedObject(), $payment2->getWrappedObject()])); + + $this($event); + + $compositeStateMachine + ->apply($payment1, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL) + ->shouldHaveBeenCalledOnce() + ; + + $compositeStateMachine + ->apply($payment2, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL) + ->shouldHaveBeenCalledOnce() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelShipmentListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelShipmentListenerSpec.php new file mode 100644 index 0000000000..4d5a86d741 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelShipmentListenerSpec.php @@ -0,0 +1,61 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]) + ; + } + + function it_cancels_shipments( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ShipmentInterface $shipment1, + ShipmentInterface $shipment2, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + $order->getShipments()->willReturn(new ArrayCollection([$shipment1->getWrappedObject(), $shipment2->getWrappedObject()])); + + $this($event); + + $compositeStateMachine + ->apply($shipment1, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CANCEL) + ->shouldHaveBeenCalledOnce() + ; + + $compositeStateMachine + ->apply($shipment2, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CANCEL) + ->shouldHaveBeenCalledOnce() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/DecrementPromotionUsagesListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/DecrementPromotionUsagesListenerSpec.php new file mode 100644 index 0000000000..362baf14db --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/DecrementPromotionUsagesListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderPromotionsUsageModifier); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_decrements_promotion_usages( + OrderPromotionsUsageModifierInterface $orderPromotionsUsageModifier, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderPromotionsUsageModifier->decrement($order)->shouldBeCalled(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/GiveBackInventoryListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/GiveBackInventoryListenerSpec.php new file mode 100644 index 0000000000..fac019f2f5 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/GiveBackInventoryListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderInventoryOperator); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_cancels_order( + OrderInventoryOperatorInterface $orderInventoryOperator, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderInventoryOperator->cancel($order)->shouldBeCalled(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/HoldInventoryListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/HoldInventoryListenerSpec.php new file mode 100644 index 0000000000..51cec5be30 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/HoldInventoryListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderInventoryOperator); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_holds_order_inventory( + OrderInventoryOperatorInterface $orderInventoryOperator, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderInventoryOperator->hold($order)->shouldBeCalled(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/IncrementPromotionUsagesListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/IncrementPromotionUsagesListenerSpec.php new file mode 100644 index 0000000000..dccd821bd8 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/IncrementPromotionUsagesListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderPromotionsUsageModifier); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_increments_promotion_usages( + OrderPromotionsUsageModifierInterface $orderPromotionsUsageModifier, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderPromotionsUsageModifier->increment($order)->shouldBeCalled(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/RequestOrderPaymentListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/RequestOrderPaymentListenerSpec.php new file mode 100644 index 0000000000..0b0d460801 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/RequestOrderPaymentListenerSpec.php @@ -0,0 +1,51 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]) + ; + } + + function it_requests_order_payment( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $compositeStateMachine + ->apply($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_REQUEST_PAYMENT) + ->shouldBeCalled() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/RequestOrderShippingListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/RequestOrderShippingListenerSpec.php new file mode 100644 index 0000000000..9dbfeb69be --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/RequestOrderShippingListenerSpec.php @@ -0,0 +1,51 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]) + ; + } + + function it_requests_order_shipping( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $compositeStateMachine + ->apply($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_REQUEST_SHIPPING) + ->shouldBeCalled() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/SaveCustomerAddressesListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/SaveCustomerAddressesListenerSpec.php new file mode 100644 index 0000000000..6ddb5976f4 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/SaveCustomerAddressesListenerSpec.php @@ -0,0 +1,47 @@ +beConstructedWith($orderAddressesSaver); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]) + ; + } + + function it_saves_addresses( + OrderAddressesSaverInterface $orderAddressesSaver, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderAddressesSaver->saveAddresses($order)->shouldBeCalled(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/SetImmutableNamesListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/SetImmutableNamesListenerSpec.php new file mode 100644 index 0000000000..1b5631986c --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/SetImmutableNamesListenerSpec.php @@ -0,0 +1,47 @@ +beConstructedWith($orderItemNamesSetter); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]) + ; + } + + function it_sets_immutable_names( + OrderItemNamesSetterInterface $orderItemNamesSetter, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderItemNamesSetter->__invoke($order)->shouldBeCalled(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/test/config/bundles.php b/src/Sylius/Bundle/CoreBundle/test/config/bundles.php index e18063af37..4b6e93e595 100644 --- a/src/Sylius/Bundle/CoreBundle/test/config/bundles.php +++ b/src/Sylius/Bundle/CoreBundle/test/config/bundles.php @@ -14,7 +14,6 @@ declare(strict_types=1); return [ Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], - Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['test_with_swiftmailer' => true], Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], diff --git a/src/Sylius/Bundle/CoreBundle/test/config/packages/test_with_swiftmailer/filesystem.yaml b/src/Sylius/Bundle/CoreBundle/test/config/packages/test_with_swiftmailer/filesystem.yaml deleted file mode 100644 index 1828fd69a2..0000000000 --- a/src/Sylius/Bundle/CoreBundle/test/config/packages/test_with_swiftmailer/filesystem.yaml +++ /dev/null @@ -1,4 +0,0 @@ -services: - test.filesystem.public: - alias: filesystem - public: true diff --git a/src/Sylius/Bundle/CoreBundle/test/config/packages/test_with_swiftmailer/swiftmailer.yaml b/src/Sylius/Bundle/CoreBundle/test/config/packages/test_with_swiftmailer/swiftmailer.yaml deleted file mode 100644 index c438f4b256..0000000000 --- a/src/Sylius/Bundle/CoreBundle/test/config/packages/test_with_swiftmailer/swiftmailer.yaml +++ /dev/null @@ -1,6 +0,0 @@ -swiftmailer: - disable_delivery: true - logging: true - spool: - type: file - path: "%kernel.cache_dir%/spool" diff --git a/src/Sylius/Bundle/CoreBundle/test/config/packages/test_with_swiftmailer/sylius_mailer.yaml b/src/Sylius/Bundle/CoreBundle/test/config/packages/test_with_swiftmailer/sylius_mailer.yaml deleted file mode 100644 index e2310f20bf..0000000000 --- a/src/Sylius/Bundle/CoreBundle/test/config/packages/test_with_swiftmailer/sylius_mailer.yaml +++ /dev/null @@ -1,2 +0,0 @@ -sylius_mailer: - sender_adapter: sylius.email_sender.adapter.swiftmailer diff --git a/src/Sylius/Bundle/CurrencyBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/CurrencyBundle/DependencyInjection/Configuration.php index ca60fd271a..85acde4ec8 100644 --- a/src/Sylius/Bundle/CurrencyBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/CurrencyBundle/DependencyInjection/Configuration.php @@ -13,6 +13,8 @@ declare(strict_types=1); namespace Sylius\Bundle\CurrencyBundle\DependencyInjection; +use Sylius\Bundle\CurrencyBundle\Doctrine\ORM\CurrencyRepository; +use Sylius\Bundle\CurrencyBundle\Doctrine\ORM\ExchangeRateRepository; use Sylius\Bundle\CurrencyBundle\Form\Type\CurrencyType; use Sylius\Bundle\CurrencyBundle\Form\Type\ExchangeRateType; use Sylius\Bundle\ResourceBundle\Controller\ResourceController; @@ -62,7 +64,7 @@ final class Configuration implements ConfigurationInterface ->scalarNode('model')->defaultValue(Currency::class)->cannotBeEmpty()->end() ->scalarNode('interface')->defaultValue(CurrencyInterface::class)->cannotBeEmpty()->end() ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() - ->scalarNode('repository')->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(CurrencyRepository::class)->cannotBeEmpty()->end() ->scalarNode('factory')->defaultValue(Factory::class)->cannotBeEmpty()->end() ->scalarNode('form')->defaultValue(CurrencyType::class)->cannotBeEmpty()->end() ->end() @@ -79,7 +81,7 @@ final class Configuration implements ConfigurationInterface ->scalarNode('model')->defaultValue(ExchangeRate::class)->cannotBeEmpty()->end() ->scalarNode('interface')->defaultValue(ExchangeRateInterface::class)->cannotBeEmpty()->end() ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() - ->scalarNode('repository')->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(ExchangeRateRepository::class)->cannotBeEmpty()->end() ->scalarNode('factory')->defaultValue(Factory::class)->cannotBeEmpty()->end() ->scalarNode('form')->defaultValue(ExchangeRateType::class)->cannotBeEmpty()->end() ->end() diff --git a/src/Sylius/Bundle/CurrencyBundle/Doctrine/ORM/CurrencyRepository.php b/src/Sylius/Bundle/CurrencyBundle/Doctrine/ORM/CurrencyRepository.php new file mode 100644 index 0000000000..90f21fe644 --- /dev/null +++ b/src/Sylius/Bundle/CurrencyBundle/Doctrine/ORM/CurrencyRepository.php @@ -0,0 +1,25 @@ + + */ +class CurrencyRepository extends EntityRepository implements CurrencyRepositoryInterface +{ +} diff --git a/src/Sylius/Bundle/OrderBundle/Resources/config/workflow/state_machine.yaml b/src/Sylius/Bundle/OrderBundle/Resources/config/workflow/state_machine.yaml new file mode 100644 index 0000000000..5a88ef99d3 --- /dev/null +++ b/src/Sylius/Bundle/OrderBundle/Resources/config/workflow/state_machine.yaml @@ -0,0 +1,25 @@ +framework: + workflows: + !php/const Sylius\Component\Order\OrderTransitions::GRAPH: + type: state_machine + marking_store: + type: method + property: state + supports: + - Sylius\Component\Order\Model\OrderInterface + initial_marking: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CART + places: + - !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CART + - !php/const Sylius\Component\Order\Model\OrderInterface::STATE_NEW + - !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CANCELLED + - !php/const Sylius\Component\Order\Model\OrderInterface::STATE_FULFILLED + transitions: + !php/const Sylius\Component\Order\OrderTransitions::TRANSITION_CREATE: + from: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CART + to: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_NEW + !php/const Sylius\Component\Order\OrderTransitions::TRANSITION_CANCEL: + from: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_NEW + to: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CANCELLED + !php/const Sylius\Component\Order\OrderTransitions::TRANSITION_FULFILL: + from: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_NEW + to: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_FULFILLED diff --git a/src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterPaypalGatewayTypePass.php b/src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterPaypalGatewayTypePass.php new file mode 100644 index 0000000000..ca0b76b679 --- /dev/null +++ b/src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterPaypalGatewayTypePass.php @@ -0,0 +1,35 @@ +removeDefinition(self::PAYPAL_GATEWAY_TYPE_SERVICE_ID); + $container->removeDefinition(self::PAYPAL_CONVERT_ACTION_SERVICE_ID); + } +} diff --git a/src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterStripeGatewayTypePass.php b/src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterStripeGatewayTypePass.php index bb886e9fae..201ff3ed7e 100644 --- a/src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterStripeGatewayTypePass.php +++ b/src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterStripeGatewayTypePass.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\PayumBundle\DependencyInjection\Compiler; -use Stripe\Stripe; +use Payum\Stripe\StripeCheckoutGatewayFactory; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -21,9 +21,9 @@ final class UnregisterStripeGatewayTypePass implements CompilerPassInterface { private const STRIPE_GATEWAY_TYPE_SERVICE_ID = 'sylius.form.type.gateway_configuration.stripe'; - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { - if (class_exists(Stripe::class)) { + if (class_exists(StripeCheckoutGatewayFactory::class)) { return; } diff --git a/src/Sylius/Bundle/PayumBundle/SyliusPayumBundle.php b/src/Sylius/Bundle/PayumBundle/SyliusPayumBundle.php index 23a2bfe697..eb0601154c 100644 --- a/src/Sylius/Bundle/PayumBundle/SyliusPayumBundle.php +++ b/src/Sylius/Bundle/PayumBundle/SyliusPayumBundle.php @@ -15,6 +15,7 @@ namespace Sylius\Bundle\PayumBundle; use Sylius\Bundle\PayumBundle\DependencyInjection\Compiler\InjectContainerIntoControllersPass; use Sylius\Bundle\PayumBundle\DependencyInjection\Compiler\RegisterGatewayConfigTypePass; +use Sylius\Bundle\PayumBundle\DependencyInjection\Compiler\UnregisterPaypalGatewayTypePass; use Sylius\Bundle\PayumBundle\DependencyInjection\Compiler\UnregisterStripeGatewayTypePass; use Sylius\Bundle\PayumBundle\DependencyInjection\Compiler\UseTweakedDoctrineStoragePass; use Sylius\Bundle\ResourceBundle\AbstractResourceBundle; @@ -37,6 +38,7 @@ final class SyliusPayumBundle extends AbstractResourceBundle $container->addCompilerPass(new InjectContainerIntoControllersPass()); $container->addCompilerPass(new RegisterGatewayConfigTypePass()); $container->addCompilerPass(new UseTweakedDoctrineStoragePass()); + $container->addCompilerPass(new UnregisterPaypalGatewayTypePass(), priority: 128); $container->addCompilerPass(new UnregisterStripeGatewayTypePass(), priority: 128); } } diff --git a/src/Sylius/Bundle/PayumBundle/composer.json b/src/Sylius/Bundle/PayumBundle/composer.json index 49c729faa9..a793b70dfa 100644 --- a/src/Sylius/Bundle/PayumBundle/composer.json +++ b/src/Sylius/Bundle/PayumBundle/composer.json @@ -30,7 +30,6 @@ "require": { "php": "^8.2", "payum/offline": "^1.7.3", - "payum/paypal-express-checkout-nvp": "^1.7.3", "payum/payum-bundle": "^2.5", "sylius/core": "^2.0", "sylius/currency": "^2.0", @@ -43,6 +42,10 @@ "phpspec/phpspec": "^7.2", "symfony/dependency-injection": "^6.3.4" }, + "suggest": { + "payum/paypal-express-checkout-nvp": "To use PayPal Express Chekout NVP", + "payum/stripe": "To use Stripe Checkout" + }, "config": { "allow-plugins": { "symfony/flex": true diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociationTypeTranslation.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociationTypeTranslation.xml index 26a3620b74..5c10b2a462 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociationTypeTranslation.xml +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociationTypeTranslation.xml @@ -26,5 +26,15 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionTranslation.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionTranslation.xml index 737c9b2df9..c0cdc43c64 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionTranslation.xml +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionTranslation.xml @@ -26,5 +26,15 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionValueTranslation.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionValueTranslation.xml index 9a78fa6a91..68b4261962 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionValueTranslation.xml +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionValueTranslation.xml @@ -19,5 +19,15 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductTranslation.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductTranslation.xml index c3b92078e2..9161372dd5 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductTranslation.xml +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductTranslation.xml @@ -60,5 +60,15 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductVariant.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductVariant.xml index 6dd5bc6d16..611b45f244 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductVariant.xml +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductVariant.xml @@ -37,5 +37,8 @@ + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductVariantTranslation.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductVariantTranslation.xml new file mode 100644 index 0000000000..661ed82f66 --- /dev/null +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductVariantTranslation.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.en.yml index d13341caa5..243426ff08 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.en.yml @@ -56,3 +56,7 @@ sylius: not_blank: Please enter association type code. regex: Association type code can only be comprised of letters, numbers, dashes and underscores. unique: The association type with given code already exists. + translation: + locale: + not_blank: Please enter the locale. + invalid: This value is not a valid locale. diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotion.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotion.xml index 37674a788c..4249c16f82 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotion.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotion.xml @@ -62,5 +62,8 @@ + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotionTranslation.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotionTranslation.xml new file mode 100644 index 0000000000..92a3a618c3 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotionTranslation.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionTranslation.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionTranslation.xml index cba8384e52..8a5e374fa8 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionTranslation.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionTranslation.xml @@ -19,5 +19,15 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.en.yml index 5fdb11feac..543d8f1f0e 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.en.yml @@ -61,3 +61,7 @@ sylius: possible_generation_amount: Invalid coupons code length or coupons amount. It is not possible to generate %expectedAmount% unique coupons with code length %codeLength%. The possible amount to generate is %possibleAmount%. usage_limit: min: Usage limit of generated coupons must be at least {{ limit }}. + translation: + locale: + not_blank: Please enter the locale. + invalid: This value is not a valid locale. diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodTranslation.xml b/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodTranslation.xml index 3998e30849..ee8118fc29 100644 --- a/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodTranslation.xml +++ b/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodTranslation.xml @@ -26,5 +26,15 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.en.yml index 9da6ebf270..f745c087c0 100644 --- a/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.en.yml @@ -30,3 +30,7 @@ sylius: shipment: shipping_method: not_blank: 'Please select shipping method.' + translation: + locale: + not_blank: 'Please enter the locale.' + invalid: 'This value is not a valid locale.' diff --git a/src/Sylius/Bundle/ShopBundle/Controller/ContactController.php b/src/Sylius/Bundle/ShopBundle/Controller/ContactController.php index 50cfca1f07..42a768e381 100644 --- a/src/Sylius/Bundle/ShopBundle/Controller/ContactController.php +++ b/src/Sylius/Bundle/ShopBundle/Controller/ContactController.php @@ -97,7 +97,7 @@ final class ContactController private function getSyliusAttribute(Request $request, string $attributeName, ?string $default): ?string { - $attributes = $request->attributes->get('_sylius'); + $attributes = $request->attributes->get('_sylius', []); return $attributes[$attributeName] ?? $default; } diff --git a/src/Sylius/Bundle/ShopBundle/Resources/private/scss/theme.scss b/src/Sylius/Bundle/ShopBundle/Resources/private/scss/theme.scss index 53ca8b4292..51eec721f1 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/private/scss/theme.scss +++ b/src/Sylius/Bundle/ShopBundle/Resources/private/scss/theme.scss @@ -738,13 +738,15 @@ header .ui.three.column.stackable.grid > .column:nth-child(3) .button span { } [data-route="sylius_shop_cart_summary"] .ui.two.column.stackable.grid, -[data-route="sylius_shop_cart_save"] .ui.two.column.stackable.grid { +[data-route="sylius_shop_cart_save"] .ui.two.column.stackable.grid, +[data-route="sylius_shop_cart_checkout"] .ui.two.column.stackable.grid { margin-top: 20px; margin-bottom: 10px; } [data-route="sylius_shop_cart_summary"] .ui.segment, -[data-route="sylius_shop_cart_save"] .ui.segment { +[data-route="sylius_shop_cart_save"] .ui.segment, +[data-route="sylius_shop_cart_checkout"] .ui.segment { border: 0; box-shadow: none; padding: 0; @@ -752,6 +754,7 @@ header .ui.three.column.stackable.grid > .column:nth-child(3) .button span { [data-route="sylius_shop_cart_summary"] .column:first-child .table tbody td, [data-route="sylius_shop_cart_save"] .column:first-child .table tbody td, +[data-route="sylius_shop_cart_checkout"] .column:first-child .table tbody td, [data-route="sylius_shop_checkout_complete"] form table tbody td, [data-route="sylius_shop_account_order_show"] table tbody td { border: 0 !important; @@ -761,6 +764,7 @@ header .ui.three.column.stackable.grid > .column:nth-child(3) .button span { [data-route="sylius_shop_cart_summary"] .column:first-child .table thead th, [data-route="sylius_shop_cart_save"] .column:first-child .table thead th, +[data-route="sylius_shop_cart_checkout"] .column:first-child .table thead th, [data-route="sylius_shop_checkout_complete"] form table thead th, [data-route="sylius_shop_account_order_show"] table thead th { border: none !important; @@ -774,6 +778,8 @@ header .ui.three.column.stackable.grid > .column:nth-child(3) .button span { [data-route="sylius_shop_cart_summary"] .column:first-child .table td:nth-child(2), [data-route="sylius_shop_cart_save"] .column:first-child .table th:nth-child(2), [data-route="sylius_shop_cart_save"] .column:first-child .table td:nth-child(2), +[data-route="sylius_shop_cart_checkout"] .column:first-child .table th:nth-child(2), +[data-route="sylius_shop_cart_checkout"] .column:first-child .table td:nth-child(2), [data-route="sylius_shop_checkout_complete"] form table th:nth-child(2), [data-route="sylius_shop_checkout_complete"] form table td:nth-child(2), [data-route="sylius_shop_account_order_show"] table th:nth-child(2), @@ -784,6 +790,7 @@ header .ui.three.column.stackable.grid > .column:nth-child(3) .button span { [data-route="sylius_shop_cart_summary"] .column:first-child .table th:nth-child(3), [data-route="sylius_shop_cart_save"] .column:first-child .table th:nth-child(3), +[data-route="sylius_shop_cart_checkout"] .column:first-child .table th:nth-child(3), [data-route="sylius_shop_checkout_complete"] form table th:nth-child(3), [data-route="sylius_shop_account_order_show"] table th:nth-child(3) { text-align: left !important; @@ -792,6 +799,7 @@ header .ui.three.column.stackable.grid > .column:nth-child(3) .button span { [data-route="sylius_shop_cart_summary"] .column:first-child .table th:nth-child(4), [data-route="sylius_shop_cart_save"] .column:first-child .table th:nth-child(4), +[data-route="sylius_shop_cart_checkout"] .column:first-child .table th:nth-child(4), [data-route="sylius_shop_checkout_complete"] form table th:nth-child(4), [data-route="sylius_shop_account_order_show"] table th:nth-child(4) { text-align: right !important; @@ -799,6 +807,7 @@ header .ui.three.column.stackable.grid > .column:nth-child(3) .button span { [data-route="sylius_shop_cart_summary"] .column:first-child .table .sylius-product-name, [data-route="sylius_shop_cart_save"] .column:first-child .table .sylius-product-name, +[data-route="sylius_shop_cart_checkout"] .column:first-child .table .sylius-product-name, [data-route="sylius_shop_checkout_complete"] form table .sylius-product-name, [data-route="sylius_shop_account_order_show"] table .sylius-product-name { font-size: 0.9em !important; @@ -808,6 +817,7 @@ header .ui.three.column.stackable.grid > .column:nth-child(3) .button span { [data-route="sylius_shop_cart_summary"] .column:first-child .table .image, [data-route="sylius_shop_cart_save"] .column:first-child .table .image, +[data-route="sylius_shop_cart_checkout"] .column:first-child .table .image, [data-route="sylius_shop_checkout_complete"] form table .image, [data-route="sylius_shop_account_order_show"] table .image { width: 100px !important; @@ -817,29 +827,34 @@ header .ui.three.column.stackable.grid > .column:nth-child(3) .button span { [data-route="sylius_shop_cart_summary"] .column:first-child .table .sub.header, [data-route="sylius_shop_cart_save"] .column:first-child .table .sub.header, +[data-route="sylius_shop_cart_checkout"] .column:first-child .table .sub.header, [data-route="sylius_shop_checkout_complete"] form table .sub.header, [data-route="sylius_shop_account_order_show"] table .sub.header { color: #adadad !important; } [data-route="sylius_shop_cart_summary"] .column:first-child .table .sylius-cart-remove-button, -[data-route="sylius_shop_cart_save"] .column:first-child .table .sylius-cart-remove-button { +[data-route="sylius_shop_cart_save"] .column:first-child .table .sylius-cart-remove-button, +[data-route="sylius_shop_cart_checkout"] .column:first-child .table .sylius-cart-remove-button { background: transparent; border: 1px solid #eee; } [data-route="sylius_shop_cart_summary"] .column:first-child .table .sylius-total, -[data-route="sylius_shop_cart_save"] .column:first-child .table .sylius-total { +[data-route="sylius_shop_cart_save"] .column:first-child .table .sylius-total, +[data-route="sylius_shop_cart_checkout"] .column:first-child .table .sylius-total { font-weight: 700; } [data-route="sylius_shop_cart_summary"] .five.column:last-child, -[data-route="sylius_shop_cart_save"] .five.column:last-child { +[data-route="sylius_shop_cart_save"] .five.column:last-child, +[data-route="sylius_shop_cart_checkout"] .five.column:last-child { padding-left: 50px !important; } [data-route="sylius_shop_cart_summary"] .column:last-child .ui.dividing.header, -[data-route="sylius_shop_cart_save"] .column:last-child .ui.dividing.header { +[data-route="sylius_shop_cart_save"] .column:last-child .ui.dividing.header, +[data-route="sylius_shop_cart_checkout"] .column:last-child .ui.dividing.header { border: 0; font-size: 1em; color: #999; @@ -850,12 +865,14 @@ header .ui.three.column.stackable.grid > .column:nth-child(3) .button span { } [data-route="sylius_shop_cart_summary"] .column:last-child .table td, -[data-route="sylius_shop_cart_save"] .column:last-child .table td { +[data-route="sylius_shop_cart_save"] .column:last-child .table td, +[data-route="sylius_shop_cart_checkout"] .column:last-child .table td { padding: 16px 0; } [data-route="sylius_shop_cart_summary"] .column:last-child .table .header td, -[data-route="sylius_shop_cart_save"] .column:last-child .table .header td { +[data-route="sylius_shop_cart_save"] .column:last-child .table .header td, +[data-route="sylius_shop_cart_checkout"] .column:last-child .table .header td { padding-top: 30px !important; } diff --git a/src/Sylius/Bundle/TaxonomyBundle/Resources/config/validation/TaxonTranslation.xml b/src/Sylius/Bundle/TaxonomyBundle/Resources/config/validation/TaxonTranslation.xml index e6ca4276c6..125d16e398 100644 --- a/src/Sylius/Bundle/TaxonomyBundle/Resources/config/validation/TaxonTranslation.xml +++ b/src/Sylius/Bundle/TaxonomyBundle/Resources/config/validation/TaxonTranslation.xml @@ -30,6 +30,16 @@ + + + + + + + + + +