Merge branch '1.13' into 2.0

* 1.13:
  [Inventory] Fix deprecation note of sylius_inventory.checker configuration
  [Inventory] Add deprecation for sylius_inventory.checker config
  Add note to the UPGRADE file about changing the service argument
  Using the service name for the availibilty checker
  [API][Test] Minor updating of functions name
  [Payum] Move validation groups configuration to PayumBundle
  Move config to Core, SyliusPaymentBundle do not requires SyliusPayumBundle
  [Api][Admin] Add ChannelNotFoundException to OrderAddressRequirementValidator
  [Api][Admin] Change validation message of address requirement
  [Api][Admin] Fix get channels test
  [Api][Checkout] Add validation for required address
  [Maintenance][API] Secure and cleanup openapi documentation modifiers
This commit is contained in:
Grzegorz Sadowski 2024-04-02 12:45:15 +02:00
commit 5d93c43eca
No known key found for this signature in database
GPG key ID: EF87FF4E6E3BD364
26 changed files with 515 additions and 117 deletions

View file

@ -817,7 +817,7 @@ final class CheckoutContext implements Context
$this->iConfirmMyOrder();
$response = $this->client->getLastResponse();
Assert::same($this->responseChecker->getError($response), 'An empty order cannot be completed.');
Assert::same($this->responseChecker->getError($response), 'An empty order cannot be processed.');
}
/**

View file

@ -22,71 +22,75 @@ final class AddressLogEntryDocumentationModifier implements DocumentationModifie
$components = $docs->getComponents();
$schemas = $components->getSchemas();
$schemas['Address-admin.address.log_entry.read'] = [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'action' => [
'readOnly' => true,
'type' => 'string',
],
'version' => [
'readOnly' => true,
'type' => 'integer',
],
'data' => [
'readOnly' => true,
'type' => 'object',
],
'logged_at' => [
'readOnly' => true,
'type' => 'string',
'format' => 'date-time',
],
],
],
];
$schemas['Address.jsonld-admin.address.log_entry.read'] = [
'type' => 'object',
'properties' => [
'hydra:member' => [
'readOnly' => true,
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'@type' => [
'readOnly' => true,
'type' => 'string',
],
'action' => [
'readOnly' => true,
'type' => 'string',
],
'version' => [
'readOnly' => true,
'type' => 'integer',
],
'data' => [
'readOnly' => true,
'type' => 'object',
],
'logged_at' => [
'readOnly' => true,
'type' => 'string',
'format' => 'date-time',
],
if (isset($schemas['Address-admin.address.log_entry.read'])) {
$schemas['Address-admin.address.log_entry.read'] = [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'action' => [
'readOnly' => true,
'type' => 'string',
],
'version' => [
'readOnly' => true,
'type' => 'integer',
],
'data' => [
'readOnly' => true,
'type' => 'object',
],
'logged_at' => [
'readOnly' => true,
'type' => 'string',
'format' => 'date-time',
],
],
],
'hydra:totalItems' => [
'readOnly' => true,
'type' => 'integer',
];
}
if (isset($schemas['Address.jsonld-admin.address.log_entry.read'])) {
$schemas['Address.jsonld-admin.address.log_entry.read'] = [
'type' => 'object',
'properties' => [
'hydra:member' => [
'readOnly' => true,
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'@type' => [
'readOnly' => true,
'type' => 'string',
],
'action' => [
'readOnly' => true,
'type' => 'string',
],
'version' => [
'readOnly' => true,
'type' => 'integer',
],
'data' => [
'readOnly' => true,
'type' => 'object',
],
'logged_at' => [
'readOnly' => true,
'type' => 'string',
'format' => 'date-time',
],
],
],
],
'hydra:totalItems' => [
'readOnly' => true,
'type' => 'integer',
],
],
],
];
];
}
$components = $components->withSchemas($schemas);

View file

@ -34,9 +34,7 @@ final class AttributeTypeDocumentationModifier implements DocumentationModifierI
$schemas = $this->updateAttributeTypesSchema($schemas);
return $docs->withComponents(
$components->withSchemas($schemas),
);
return $docs->withComponents($components->withSchemas($schemas));
}
/**
@ -56,6 +54,10 @@ final class AttributeTypeDocumentationModifier implements DocumentationModifierI
];
foreach ($schemasToBeUpdated as $schemaToBeUpdated) {
if (!isset($schemas[$schemaToBeUpdated])) {
continue;
}
$schemas[$schemaToBeUpdated]['properties']['type'] = [
'type' => 'string',
'enum' => $attributeTypes,

View file

@ -18,7 +18,6 @@ use ApiPlatform\OpenApi\OpenApi;
final class OrderAdjustmentsTypeDocumentationModifier implements DocumentationModifierInterface
{
public const PATH = '%s/admin/orders/{tokenValue}/adjustments';
public function __construct(private string $apiRoute, private string $adjustmentResourceClass)
{
@ -26,11 +25,14 @@ final class OrderAdjustmentsTypeDocumentationModifier implements DocumentationMo
public function modify(OpenApi $docs): OpenApi
{
$paths = $docs->getPaths();
$path = sprintf('%s/admin/orders/{tokenValue}/adjustments', $this->apiRoute);
$path = sprintf(self::PATH, $this->apiRoute);
$paths = $docs->getPaths();
$pathItem = $paths->getPath($path);
$operation = $pathItem->getGet();
$operation = $pathItem?->getGet();
if (null === $operation) {
return $docs;
}
$parameters = $operation->getParameters();
$parameters[] = new Parameter(

View file

@ -19,8 +19,6 @@ use Sylius\Component\Review\Model\ReviewInterface;
final class ProductReviewDocumentationModifier implements DocumentationModifierInterface
{
public const PATH = '%s/admin/product-reviews';
public function __construct(
private string $apiRoute,
) {
@ -28,11 +26,14 @@ final class ProductReviewDocumentationModifier implements DocumentationModifierI
public function modify(OpenApi $docs): OpenApi
{
$paths = $docs->getPaths();
$path = sprintf('%s/admin/product-reviews', $this->apiRoute);
$path = sprintf(self::PATH, $this->apiRoute);
$paths = $docs->getPaths();
$pathItem = $paths->getPath($path);
$operation = $pathItem->getGet();
$operation = $pathItem?->getGet();
if (null === $operation) {
return $docs;
}
$parameters = $operation->getParameters();
$parameters = array_filter(

View file

@ -29,7 +29,6 @@ final class ProductSlugDocumentationModifier implements DocumentationModifierInt
$paths = $docs->getPaths();
$pathItem = $paths->getPath($path);
$operation = $pathItem?->getGet();
if (null === $operation) {
return $docs;
}

View file

@ -47,7 +47,10 @@ final class PromotionDocumentationModifier implements DocumentationModifierInter
{
$pathItem = $paths->getPath($path);
$methodGet = sprintf('get%s', $method);
$operation = $pathItem->$methodGet();
$operation = $pathItem?->$methodGet();
if (null === $operation) {
return;
}
$description = sprintf(
"%s\n\n Allowed rule types: `%s` \n\n Allowed action types: `%s`",

View file

@ -38,36 +38,38 @@ final class ShippingMethodDocumentationModifier implements DocumentationModifier
$components = $docs->getComponents();
$schemas = $components->getSchemas();
if (!isset($schemas['ShippingMethod.jsonld-shop.shipping_method.read'])) {
return $docs;
if (isset($schemas['ShippingMethod.jsonld-shop.shipping_method.read'])) {
$schemas['ShippingMethod.jsonld-shop.shipping_method.read']['properties']['price'] = [
'type' => 'integer',
'readOnly' => true,
'default' => 0,
];
$components = $components->withSchemas($schemas);
$docs = $docs->withComponents($components);
}
$schemas['ShippingMethod.jsonld-shop.shipping_method.read']['properties']['price'] = [
'type' => 'integer',
'readOnly' => true,
'default' => 0,
];
$this->modifyDescription($docs);
return $docs->withComponents(
$components->withSchemas($schemas),
)->withPaths($docs->getPaths());
return $this->modifyDescription($docs);
}
private function modifyDescription(OpenApi $docs): void
private function modifyDescription(OpenApi $docs): OpenApi
{
$paths = $docs->getPaths();
$this->addDescription($paths, sprintf('%s%s', $this->apiRoute, self::ROUTE_ADMIN_SHIPPING_METHODS), 'Post');
$this->addDescription($paths, sprintf('%s%s', $this->apiRoute, self::ROUTE_ADMIN_SHIPPING_METHOD), 'Put');
return $docs->withPaths($paths);
}
private function addDescription(Paths $paths, string $path, string $method): void
{
$pathItem = $paths->getPath($path);
$methodGet = sprintf('get%s', $method);
$operation = $pathItem->$methodGet();
$operation = $pathItem?->$methodGet();
if (null === $operation) {
return;
}
$description = sprintf(
"%s\n\n Allowed rule types: `%s` \n\n Allowed calculators: `%s`",

View file

@ -22,8 +22,6 @@ use Symfony\Component\HttpFoundation\Response;
final class StatisticsDocumentationModifier implements DocumentationModifierInterface
{
private const PATH = '/admin/statistics';
/** @param array<string, array<string, string>> $intervalsMap */
public function __construct(
private string $apiRoute,
@ -82,7 +80,7 @@ final class StatisticsDocumentationModifier implements DocumentationModifierInte
],
];
$path = $this->apiRoute . self::PATH;
$path = sprintf('%s/admin/statistics', $this->apiRoute);
$paths = $docs->getPaths();
$paths->addPath($path, $this->getPathItem());

View file

@ -188,5 +188,10 @@
<argument type="service" id="Sylius\Bundle\ApiBundle\Validator\CatalogPromotion\ForVariantsScopeValidator.inner" />
<argument type="service" id="Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface" />
</service>
<service id="sylius.validator.order_address_requirement" class="Sylius\Bundle\ApiBundle\Validator\Constraints\OrderAddressRequirementValidator">
<argument type="service" id="sylius.repository.order" />
<tag name="validator.constraint_validator" alias="sylius_order_address_requirement_validator" />
</service>
</services>
</container>

View file

@ -13,20 +13,17 @@
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/services/constraint-mapping-1.0.xsd">
<class name="Sylius\Bundle\ApiBundle\Command\Checkout\UpdateCart">
<constraint name="Sylius\Bundle\ApiBundle\Validator\Constraints\OrderAddressRequirement">
<option name="groups">sylius</option>
</constraint>
<constraint name="Sylius\Bundle\ApiBundle\Validator\Constraints\CorrectOrderAddress">
<option name="groups">
<value>sylius</value>
</option>
<option name="groups">sylius</option>
</constraint>
<constraint name="Sylius\Bundle\ApiBundle\Validator\Constraints\PromotionCouponEligibility">
<option name="groups">
<value>sylius</value>
</option>
<option name="groups">sylius</option>
</constraint>
<constraint name="Sylius\Bundle\ApiBundle\Validator\Constraints\OrderNotEmpty">
<option name="groups">
<value>sylius</value>
</option>
<option name="groups">sylius</option>
</constraint>
<property name="billingAddress">
<constraint name="Valid" />

View file

@ -19,7 +19,7 @@ sylius:
date_time:
invalid: 'The date time is not valid ISO 8601 date time in Y-m-d\TH:i:s format.'
order:
not_empty: 'An empty order cannot be completed.'
not_empty: 'An empty order cannot be processed.'
payment_method:
cannot_change_payment_method_for_cancelled_order: 'You cannot change the payment method for a cancelled order.'
not_available: 'The payment method %name% is not available for this order. Please choose another one.'

View file

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

View file

@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
use Sylius\Bundle\ApiBundle\Command\Checkout\UpdateCart;
use Sylius\Bundle\ApiBundle\Exception\ChannelNotFoundException;
use Sylius\Component\Channel\Repository\ChannelRepositoryInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
use Webmozart\Assert\Assert;
final class OrderAddressRequirementValidator extends ConstraintValidator
{
/** @param OrderRepositoryInterface<OrderInterface> $orderRepository */
public function __construct(private readonly OrderRepositoryInterface $orderRepository)
{
}
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof OrderAddressRequirement) {
throw new UnexpectedTypeException($constraint, OrderAddressRequirement::class);
}
if (!$value instanceof UpdateCart) {
throw new UnexpectedValueException($value, UpdateCart::class);
}
if (null === $value->billingAddress && null === $value->shippingAddress) {
return;
}
/** @var OrderInterface|null $order */
$order = $this->orderRepository->findCartByTokenValue($value->orderTokenValue);
$channel = $order?->getChannel();
if (null === $channel) {
throw new ChannelNotFoundException();
}
[$method, $addressName] = $channel->isShippingAddressInCheckoutRequired() ? ['getShippingAddress', 'shipping address'] : ['getBillingAddress', 'billing address'];
if (null === $value->$method()) {
$this->context->addViolation($constraint->message, ['%addressName%' => $addressName]);
}
}
}

View file

@ -0,0 +1,178 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\Validator\Constraints;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\ApiBundle\Command\Checkout\UpdateCart;
use Sylius\Bundle\ApiBundle\Exception\ChannelNotFoundException;
use Sylius\Bundle\ApiBundle\Validator\Constraints\OrderAddressRequirement;
use Sylius\Component\Core\Model\AddressInterface;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
final class OrderAddressRequirementValidatorSpec extends ObjectBehavior
{
const MESSAGE = 'sylius.order.address_requirement';
function let(OrderRepositoryInterface $orderRepository, ExecutionContextInterface $context): void
{
$this->beConstructedWith($orderRepository);
$this->initialize($context);
}
function it_throws_an_exception_if_constraint_is_not_an_instance_of_order_address_requirement(
Constraint $constraint,
): void {
$this
->shouldThrow(UnexpectedTypeException::class)
->during('validate', ['product_code', $constraint])
;
}
function it_throws_an_exception_if_value_is_not_an_instance_of_update_cart(
OrderInterface $order,
): void {
$this
->shouldThrow(UnexpectedValueException::class)
->during('validate', [$order, new OrderAddressRequirement()])
;
}
function it_does_nothing_if_billing_and_shipping_addresses_are_not_provided(
ExecutionContextInterface $context,
OrderInterface $order,
ChannelInterface $channel,
): void {
$updateCart = new UpdateCart();
$this->validate($updateCart, new OrderAddressRequirement());
$context->addViolation(self::MESSAGE, ['%addressName%' => 'billingAddress'])->shouldNotHaveBeenCalled();
$context->addViolation(self::MESSAGE, ['%addressName%' => 'shippingAddress'])->shouldNotHaveBeenCalled();
}
function it_throws_an_exception_if_order_is_not_found(
OrderRepositoryInterface $orderRepository,
AddressInterface $billingAddress,
): void {
$orderRepository->findCartByTokenValue('TOKEN')->willReturn(null);
$updateCart = new UpdateCart(billingAddress: $billingAddress->getWrappedObject());
$updateCart->setOrderTokenValue('TOKEN');
$this
->shouldThrow(ChannelNotFoundException::class)
->during('validate', [$updateCart, new OrderAddressRequirement()])
;
}
function it_throws_an_exception_if_order_does_not_have_channel(
OrderRepositoryInterface $orderRepository,
OrderInterface $order,
AddressInterface $billingAddress,
): void {
$orderRepository->findCartByTokenValue('TOKEN')->willReturn($order);
$order->getChannel()->willReturn(null);
$updateCart = new UpdateCart(billingAddress: $billingAddress->getWrappedObject());
$updateCart->setOrderTokenValue('TOKEN');
$this
->shouldThrow(ChannelNotFoundException::class)
->during('validate', [$updateCart, new OrderAddressRequirement()])
;
}
function it_does_nothing_if_shipping_address_is_required_and_provided(
OrderRepositoryInterface $orderRepository,
ExecutionContextInterface $context,
OrderInterface $order,
ChannelInterface $channel,
AddressInterface $shippingAddress,
): void {
$orderRepository->findCartByTokenValue('TOKEN')->willReturn($order);
$order->getChannel()->willReturn($channel);
$channel->isShippingAddressInCheckoutRequired()->willReturn(true);
$updateCart = new UpdateCart(shippingAddress: $shippingAddress->getWrappedObject());
$updateCart->setOrderTokenValue('TOKEN');
$this->validate($updateCart, new OrderAddressRequirement());
$context->addViolation(self::MESSAGE, ['%addressName%' => 'shippingAddress'])->shouldNotHaveBeenCalled();
}
function it_does_nothing_if_billing_address_is_required_and_provided(
OrderRepositoryInterface $orderRepository,
ExecutionContextInterface $context,
OrderInterface $order,
ChannelInterface $channel,
AddressInterface $billingAddress,
): void {
$orderRepository->findCartByTokenValue('TOKEN')->willReturn($order);
$order->getChannel()->willReturn($channel);
$channel->isShippingAddressInCheckoutRequired()->willReturn(false);
$updateCart = new UpdateCart(billingAddress: $billingAddress->getWrappedObject());
$updateCart->setOrderTokenValue('TOKEN');
$this->validate($updateCart, new OrderAddressRequirement());
$context->addViolation(self::MESSAGE, ['%addressName%' => 'billingAddress'])->shouldNotHaveBeenCalled();
}
function it_adds_violation_if_shipping_address_is_required_but_not_provided(
OrderRepositoryInterface $orderRepository,
ExecutionContextInterface $context,
OrderInterface $order,
AddressInterface $billingAddress,
ChannelInterface $channel,
): void {
$orderRepository->findCartByTokenValue('TOKEN')->willReturn($order);
$order->getChannel()->willReturn($channel);
$channel->isShippingAddressInCheckoutRequired()->willReturn(true);
$updateCart = new UpdateCart(billingAddress: $billingAddress->getWrappedObject());
$updateCart->setOrderTokenValue('TOKEN');
$this->validate($updateCart, new OrderAddressRequirement());
$context->addViolation(self::MESSAGE, ['%addressName%' => 'shipping address'])->shouldHaveBeenCalled();
}
function it_adds_violation_if_billing_address_is_required_but_not_provided(
OrderRepositoryInterface $orderRepository,
ExecutionContextInterface $context,
OrderInterface $order,
AddressInterface $shippingAddress,
ChannelInterface $channel,
): void {
$orderRepository->findCartByTokenValue('TOKEN')->willReturn($order);
$order->getChannel()->willReturn($channel);
$channel->isShippingAddressInCheckoutRequired()->willReturn(false);
$updateCart = new UpdateCart(shippingAddress: $shippingAddress->getWrappedObject());
$updateCart->setOrderTokenValue('TOKEN');
$this->validate($updateCart, new OrderAddressRequirement());
$context->addViolation(self::MESSAGE, ['%addressName%' => 'billing address'])->shouldHaveBeenCalled();
}
}

View file

@ -11,6 +11,7 @@ imports:
- { resource: "@SyliusLocaleBundle/Resources/config/app/config.yml" }
- { resource: "@SyliusOrderBundle/Resources/config/app/config.yml" }
- { resource: "@SyliusPaymentBundle/Resources/config/app/config.yml" }
- { resource: "@SyliusPayumBundle/Resources/config/app/config.yaml" }
- { resource: "@SyliusProductBundle/Resources/config/app/config.yml" }
- { resource: "@SyliusPromotionBundle/Resources/config/app/config.yml" }
- { resource: "@SyliusShippingBundle/Resources/config/app/config.yml" }

View file

@ -110,7 +110,7 @@
</service>
<service id="Sylius\Bundle\CoreBundle\EventListener\PaymentPreCompleteListener">
<argument type="service" id="Sylius\Component\Inventory\Checker\AvailabilityCheckerInterface" />
<argument type="service" id="sylius.availability_checker" />
<tag name="kernel.event_listener" event="sylius.payment.pre_complete" method="checkStockAvailability" />
</service>

View file

@ -93,6 +93,7 @@ sylius:
insufficient_stock: 'Insufficient stock'
max_integer: 'Value must be less than {{ compared_value }}.'
order:
address_requirement: 'Please provide a %addressName%.'
currency_code:
not_valid: The currency code you entered is invalid.
invalid_state_transition: 'Cannot complete as order is in a wrong state. Current: %currentState%. Possible transitions: %possibleTransitions%.'

View file

@ -34,7 +34,11 @@ final class Configuration implements ConfigurationInterface
->addDefaultsIfNotSet()
->children()
->scalarNode('driver')->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM)->end()
->scalarNode('checker')->defaultValue('sylius.availability_checker.default')->cannotBeEmpty()->end()
->scalarNode('checker')
->setDeprecated('sylius/inventory-bundle', '1.13', 'The "%path%.%node%" is deprecated and will be removed in 2.0.')
->defaultValue('sylius.availability_checker.default')
->cannotBeEmpty()
->end()
->end()
;

View file

@ -7,13 +7,3 @@ jms_serializer:
sylius-payment:
namespace_prefix: "Sylius\\Component\\Payment"
path: "@SyliusPaymentBundle/Resources/config/serializer"
sylius_payum:
gateway_config:
validation_groups:
paypal_express_checkout:
- 'sylius'
- 'sylius_paypal_express_checkout'
stripe_checkout:
- 'sylius'
- 'sylius_stripe_checkout'

View file

@ -0,0 +1,12 @@
# This file is part of the Sylius package.
# (c) Sylius Sp. z o.o.
sylius_payum:
gateway_config:
validation_groups:
paypal_express_checkout:
- 'sylius'
- 'sylius_paypal_express_checkout'
stripe_checkout:
- 'sylius'
- 'sylius_stripe_checkout'

View file

@ -32,6 +32,7 @@ Sylius\Component\Core\Model\ProductVariant:
optionValues: ['@product_option_value_color_blue']
channelPricings:
WEB: '@channel_pricing_product_variant_mug_blue_web'
MOBILE: '@channel_pricing_product_variant_mug_blue_mobile'
product_variant_mug_nft:
channelPricings:
WEB: '@channel_pricing_product_variant_mug_nft_web'
@ -77,6 +78,9 @@ Sylius\Component\Core\Model\ChannelPricing:
channel_pricing_product_variant_free_mug_nft_web:
channelCode: 'WEB'
price: 0
channel_pricing_product_variant_mug_blue_mobile:
channelCode: 'MOBILE'
price: 2000
Sylius\Component\Product\Model\ProductOption:
product_option_color:

View file

@ -27,6 +27,7 @@ Sylius\Component\Core\Model\Channel:
taxCalculationStrategy: 'order_items_based'
channelPriceHistoryConfig: '@mobile_price_history_config'
shopBillingData: '@billing_data'
shippingAddressInCheckoutRequired: true
Sylius\Component\Core\Model\ChannelPriceHistoryConfig:
web_price_history_config:

View file

@ -58,7 +58,7 @@
"skippingShippingStepAllowed": false,
"skippingPaymentStepAllowed": false,
"accountVerificationRequired": true,
"shippingAddressInCheckoutRequired": false,
"shippingAddressInCheckoutRequired": true,
"shopBillingData": "\/api\/v2\/admin\/shop-billing-datas\/@integer@",
"menuTaxon": null,
"channelPriceHistoryConfig": "\/api\/v2\/admin\/channel-price-history-configs\/@integer@"

View file

@ -29,6 +29,108 @@ final class CheckoutCompletionTest extends JsonApiTestCase
parent::setUp();
}
/** @test */
public function it_does_not_allow_updating_without_items(): void
{
$this->loadFixturesFromFiles(['channel.yaml', 'cart.yaml']);
$tokenValue = $this->pickUpCart();
$this->client->request(
method: 'PUT',
uri: sprintf('/api/v2/shop/orders/%s', $tokenValue),
server: self::CONTENT_TYPE_HEADER,
content: json_encode([
'email' => 'oliver@doe.com',
], \JSON_THROW_ON_ERROR),
);
$this->assertResponseViolations(
$this->client->getResponse(),
[
['propertyPath' => '', 'message' => 'An empty order cannot be processed.'],
],
);
}
/** @test */
public function it_does_not_allow_updating_without_require_billing_address(): void
{
$this->loadFixturesFromFiles([
'channel.yaml',
'cart.yaml',
'country.yaml',
]);
$tokenValue = $this->pickUpCart();
$this->addItemToCart('MUG_BLUE', 3, $tokenValue);
$this->client->request(
method: 'PUT',
uri: sprintf('/api/v2/shop/orders/%s', $tokenValue),
server: self::CONTENT_TYPE_HEADER,
content: json_encode([
'email' => 'oliver@doe.com',
'shippingAddress' => [
'firstName' => 'Oliver',
'lastName' => 'Doe',
'phoneNumber' => '123456789',
'countryCode' => 'US',
'provinceCode' => 'US-MI',
'city' => 'New York',
'street' => 'Broadway',
'postcode' => '10001',
],
], \JSON_THROW_ON_ERROR),
);
$this->assertResponseViolations(
$this->client->getResponse(),
[
['propertyPath' => '', 'message' => 'Please provide a billing address.'],
]
);
}
/** @test */
public function it_does_not_allow_updating_without_require_shipping_address(): void
{
$this->loadFixturesFromFiles([
'channel.yaml',
'cart.yaml',
'country.yaml',
]);
$tokenValue = $this->pickUpCart(channelCode: 'MOBILE');
$this->addItemToCart('MUG_BLUE', 3, $tokenValue);
$this->client->request(
method: 'PUT',
uri: sprintf('/api/v2/shop/orders/%s', $tokenValue),
server: self::CONTENT_TYPE_HEADER,
content: json_encode([
'email' => 'oliver@doe.com',
'billingAddress' => [
'firstName' => 'Oliver',
'lastName' => 'Doe',
'phoneNumber' => '123456789',
'countryCode' => 'US',
'provinceCode' => 'US-MI',
'city' => 'New York',
'street' => 'Broadway',
'postcode' => '10001',
],
], \JSON_THROW_ON_ERROR),
);
$this->assertResponseViolations(
$this->client->getResponse(),
[
['propertyPath' => '', 'message' => 'Please provide a shipping address.'],
]
);
}
/** @test */
public function it_prevents_from_order_completion_if_order_is_in_the_cart_state(): void
{