diff --git a/doc/swagger.yml b/doc/swagger.yml
index 835a85674..60603090c 100644
--- a/doc/swagger.yml
+++ b/doc/swagger.yml
@@ -394,7 +394,7 @@ paths:
name: "content"
required: true
schema:
- $ref: "#/definitions/CheckoutChoosePaymentMethodRequest"
+ $ref: "#/definitions/ChoosePaymentMethodRequest"
responses:
204:
description: "Payment method has been chosen."
@@ -838,6 +838,51 @@ paths:
security:
- bearerAuth: []
+ /orders/{token}/payment:
+ parameters:
+ - $ref: "#/parameters/CartToken"
+ get:
+ tags:
+ - "order"
+ summary: "Get available payment methods."
+ description: "This endpoint will show you available payment methods for an order."
+ operationId: "showAvailablePaymentMethods"
+ responses:
+ 200:
+ description: "Get available payment methods."
+ schema:
+ $ref: "#/definitions/AvailablePaymentMethods"
+ 400:
+ description: "Invalid input, validation failed."
+ schema:
+ $ref: "#/definitions/GeneralError"
+ /orders/{token}/payment/{id}:
+ parameters:
+ - $ref: "#/parameters/CartToken"
+ put:
+ tags:
+ - "order"
+ summary: "Choosing cart payment method."
+ description: "This endpoint will allow you to update an order payment method."
+ operationId: "updatePaymentMethod"
+ parameters:
+ - name: "id"
+ in: "path"
+ description: "Order number of payment for which payment method should be specified."
+ required: true
+ type: "string"
+ - in: "body"
+ name: "content"
+ required: true
+ schema:
+ $ref: "#/definitions/ChoosePaymentMethodRequest"
+ responses:
+ 204:
+ description: "Payment method has been chosen."
+ 400:
+ description: "Invalid input, validation failed."
+ schema:
+ $ref: "#/definitions/GeneralError"
/me:
get:
tags:
@@ -1116,7 +1161,7 @@ definitions:
type: "string"
description: "Code of chosen shipping method."
example: "DHL"
- CheckoutChoosePaymentMethodRequest:
+ ChoosePaymentMethodRequest:
type: "object"
description: "Body of request used to choose payment method."
required:
@@ -1808,6 +1853,20 @@ definitions:
description: "Date the order was completed in ISO 8601 format."
type: "string"
format: "date-time"
+ paymentState:
+ description: "Current payment state of an order."
+ type: "string"
+ example: "awaiting_payment"
+ enum:
+ - "cart"
+ - "awaiting_payment"
+ - "partially_authorized"
+ - "authorized"
+ - "partially_paid"
+ - "cancelled"
+ - "paid"
+ - "partially_refunded"
+ - "refunded"
items:
type: "array"
items:
diff --git a/spec/Command/Order/UpdatePaymentMethodSpec.php b/spec/Command/Order/UpdatePaymentMethodSpec.php
new file mode 100644
index 000000000..8eaa45c76
--- /dev/null
+++ b/spec/Command/Order/UpdatePaymentMethodSpec.php
@@ -0,0 +1,30 @@
+beConstructedWith('ORDERTOKEN', 1, 'CASH_ON_DELIVERY_METHOD');
+ }
+
+ function it_has_order_token(): void
+ {
+ $this->orderToken()->shouldReturn('ORDERTOKEN');
+ }
+
+ function it_has_identifier_of_payment(): void
+ {
+ $this->paymentId()->shouldReturn(1);
+ }
+
+ function it_has_payment_method_defined(): void
+ {
+ $this->paymentMethodCode()->shouldReturn('CASH_ON_DELIVERY_METHOD');
+ }
+}
diff --git a/spec/Factory/Order/PlacedOrderViewFactorySpec.php b/spec/Factory/Order/PlacedOrderViewFactorySpec.php
index 757f44058..5c617f133 100644
--- a/spec/Factory/Order/PlacedOrderViewFactorySpec.php
+++ b/spec/Factory/Order/PlacedOrderViewFactorySpec.php
@@ -11,6 +11,7 @@
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\OrderItemInterface;
use Sylius\Component\Core\OrderCheckoutStates;
+use Sylius\Component\Core\OrderPaymentStates;
use Sylius\ShopApiPlugin\Factory\AddressBook\AddressViewFactoryInterface;
use Sylius\ShopApiPlugin\Factory\Cart\AdjustmentViewFactoryInterface;
use Sylius\ShopApiPlugin\Factory\Cart\CartItemViewFactoryInterface;
@@ -64,6 +65,7 @@ function it_creates_a_placed_order_view(
$cart->getCurrencyCode()->willReturn('GBP');
$cart->getCheckoutState()->willReturn(OrderCheckoutStates::STATE_COMPLETED);
$cart->getCheckoutCompletedAt()->willReturn(new \DateTime('2019-02-15T15:00:00+00:00'));
+ $cart->getPaymentState()->willReturn(OrderPaymentStates::STATE_AWAITING_PAYMENT);
$cart->getTokenValue()->willReturn('ORDER_TOKEN');
$cart->getNumber()->willReturn('ORDER_NUMBER');
$cart->getShippingTotal()->willReturn(500);
@@ -99,6 +101,7 @@ function it_creates_a_placed_order_view(
$placedOrderView->locale = 'en_GB';
$placedOrderView->checkoutState = OrderCheckoutStates::STATE_COMPLETED;
$placedOrderView->checkoutCompletedAt = '2019-02-15T15:00:00+00:00';
+ $placedOrderView->paymentState = OrderPaymentStates::STATE_AWAITING_PAYMENT;
$placedOrderView->items = [new ItemView()];
$placedOrderView->totals = new TotalsView();
diff --git a/spec/Handler/Order/UpdatePaymentMethodHandlerSpec.php b/spec/Handler/Order/UpdatePaymentMethodHandlerSpec.php
new file mode 100644
index 000000000..f36b4d252
--- /dev/null
+++ b/spec/Handler/Order/UpdatePaymentMethodHandlerSpec.php
@@ -0,0 +1,168 @@
+beConstructedWith($orderRepository, $paymentMethodRepository);
+ }
+
+ function it_assigns_chosen_payment_method_to_specified_payment(
+ OrderRepositoryInterface $orderRepository,
+ OrderInterface $order,
+ PaymentMethodRepositoryInterface $paymentMethodRepository,
+ PaymentMethodInterface $paymentMethod,
+ PaymentInterface $payment
+ ): void {
+ $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order);
+ $order->getState()->willReturn(OrderInterface::STATE_NEW);
+ $order->getPayments()->willReturn(new ArrayCollection([$payment->getWrappedObject()]));
+ $order->getPaymentState()->willReturn(OrderPaymentStates::STATE_AWAITING_PAYMENT);
+ $paymentMethodRepository->findOneBy(['code' => 'CASH_ON_DELIVERY_METHOD'])->willReturn($paymentMethod);
+ $payment->setMethod($paymentMethod)->shouldBeCalled();
+ $payment->getState()->willReturn(PaymentInterface::STATE_NEW);
+
+ $this(new UpdatePaymentMethod('ORDERTOKEN', 0, 'CASH_ON_DELIVERY_METHOD'));
+ }
+
+ function it_throws_an_exception_if_order_with_given_token_has_not_been_found(
+ OrderRepositoryInterface $orderRepository,
+ PaymentInterface $payment
+ ): void {
+ $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn(null);
+ $payment->setMethod(Argument::type(PaymentMethodInterface::class))->shouldNotBeCalled();
+
+ $this
+ ->shouldThrow(\InvalidArgumentException::class)
+ ->during('__invoke', [
+ new UpdatePaymentMethod('ORDERTOKEN', 0, 'CASH_ON_DELIVERY_METHOD'),
+ ])
+ ;
+ }
+
+ function it_throws_an_exception_if_order_is_cart(
+ OrderRepositoryInterface $orderRepository,
+ OrderInterface $order,
+ PaymentMethodRepositoryInterface $paymentMethodRepository,
+ PaymentInterface $payment
+ ): void {
+ $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order);
+ $order->getState()->willReturn(OrderInterface::STATE_CART);
+
+ $paymentMethodRepository->findOneBy(Argument::any())->shouldNotBeCalled();
+ $payment->setMethod(Argument::type(PaymentMethodInterface::class))->shouldNotBeCalled();
+
+ $this
+ ->shouldThrow(\InvalidArgumentException::class)
+ ->during('__invoke', [
+ new UpdatePaymentMethod('ORDERTOKEN', 0, 'CASH_ON_DELIVERY_METHOD'),
+ ])
+ ;
+ }
+
+ function it_throws_an_exception_if_order_cannot_have_payment_updated(
+ OrderRepositoryInterface $orderRepository,
+ OrderInterface $order,
+ PaymentMethodRepositoryInterface $paymentMethodRepository,
+ PaymentInterface $payment
+ ): void {
+ $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order);
+ $order->getState()->willReturn(OrderInterface::STATE_NEW);
+ $order->getPaymentState()->willReturn(OrderPaymentStates::STATE_PAID);
+
+ $paymentMethodRepository->findOneBy(Argument::any())->shouldNotBeCalled();
+ $payment->setMethod(Argument::type(PaymentMethodInterface::class))->shouldNotBeCalled();
+
+ $this
+ ->shouldThrow(\InvalidArgumentException::class)
+ ->during('__invoke', [
+ new UpdatePaymentMethod('ORDERTOKEN', 0, 'CASH_ON_DELIVERY_METHOD'),
+ ])
+ ;
+ }
+
+ function it_throws_an_exception_if_payment_method_with_given_code_has_not_been_found(
+ OrderRepositoryInterface $orderRepository,
+ OrderInterface $order,
+ PaymentMethodRepositoryInterface $paymentMethodRepository,
+ PaymentInterface $payment
+ ): void {
+ $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order);
+ $order->getState()->willReturn(OrderInterface::STATE_NEW);
+ $order->getPaymentState()->willReturn(OrderPaymentStates::STATE_AWAITING_PAYMENT);
+ $paymentMethodRepository->findOneBy(['code' => 'CASH_ON_DELIVERY_METHOD'])->willReturn(null);
+
+ $payment->setMethod(Argument::type(PaymentMethodInterface::class))->shouldNotBeCalled();
+
+ $this
+ ->shouldThrow(\InvalidArgumentException::class)
+ ->during('__invoke', [
+ new UpdatePaymentMethod('ORDERTOKEN', 0, 'CASH_ON_DELIVERY_METHOD'),
+ ])
+ ;
+ }
+
+ function it_throws_an_exception_if_ordered_payment_has_not_been_found(
+ OrderRepositoryInterface $orderRepository,
+ OrderInterface $order,
+ PaymentMethodRepositoryInterface $paymentMethodRepository,
+ PaymentMethodInterface $paymentMethod,
+ PaymentInterface $payment
+ ): void {
+ $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order);
+ $order->getState()->willReturn(OrderInterface::STATE_NEW);
+ $order->getPaymentState()->willReturn(OrderPaymentStates::STATE_AWAITING_PAYMENT);
+ $paymentMethodRepository->findOneBy(['code' => 'CASH_ON_DELIVERY_METHOD'])->willReturn($paymentMethod);
+ $order->getPayments()->willReturn(new ArrayCollection([]));
+
+ $payment->setMethod(Argument::type(PaymentMethodInterface::class))->shouldNotBeCalled();
+
+ $this
+ ->shouldThrow(\InvalidArgumentException::class)
+ ->during('__invoke', [
+ new UpdatePaymentMethod('ORDERTOKEN', 0, 'CASH_ON_DELIVERY_METHOD'),
+ ])
+ ;
+ }
+
+ function it_throws_an_exception_if_ordered_payment_is_not_new(
+ OrderRepositoryInterface $orderRepository,
+ OrderInterface $order,
+ PaymentMethodRepositoryInterface $paymentMethodRepository,
+ PaymentMethodInterface $paymentMethod,
+ PaymentInterface $payment
+ ): void {
+ $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order);
+ $order->getState()->willReturn(OrderInterface::STATE_NEW);
+ $order->getPaymentState()->willReturn(OrderPaymentStates::STATE_AWAITING_PAYMENT);
+ $paymentMethodRepository->findOneBy(['code' => 'CASH_ON_DELIVERY_METHOD'])->willReturn($paymentMethod);
+ $order->getPayments()->willReturn(new ArrayCollection([$payment->getWrappedObject()]));
+
+ $payment->getState()->willReturn(PaymentInterface::STATE_CART);
+ $payment->setMethod(Argument::type(PaymentMethodInterface::class))->shouldNotBeCalled();
+
+ $this
+ ->shouldThrow(\InvalidArgumentException::class)
+ ->during('__invoke', [
+ new UpdatePaymentMethod('ORDERTOKEN', 0, 'CASH_ON_DELIVERY_METHOD'),
+ ])
+ ;
+ }
+}
diff --git a/spec/Validator/Order/OrderExistsValidatorSpec.php b/spec/Validator/Order/OrderExistsValidatorSpec.php
new file mode 100644
index 000000000..c76b98772
--- /dev/null
+++ b/spec/Validator/Order/OrderExistsValidatorSpec.php
@@ -0,0 +1,63 @@
+beConstructedWith($orderRepository);
+
+ $this->initialize($executionContext);
+ }
+
+ function it_does_not_add_constraint_if_order_exists(
+ OrderInterface $order,
+ OrderRepositoryInterface $orderRepository,
+ ExecutionContextInterface $executionContext
+ ): void {
+ $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN', 'state' => OrderInterface::STATE_NEW])->willReturn($order);
+
+ $executionContext->addViolation(Argument::any())->shouldNotBeCalled();
+
+ $this->validate('ORDERTOKEN', new OrderExists(['state' => OrderInterface::STATE_NEW]));
+ }
+
+ function it_does_not_add_constraint_if_order_exists_multi_state(
+ OrderInterface $order,
+ OrderRepositoryInterface $orderRepository,
+ ExecutionContextInterface $executionContext
+ ): void {
+ $orderRepository->findOneBy(
+ [
+ 'tokenValue' => 'ORDERTOKEN',
+ 'state' => [OrderInterface::STATE_NEW, 'other_state'],
+ ])->willReturn($order);
+
+ $executionContext->addViolation(Argument::any())->shouldNotBeCalled();
+
+ $this->validate('ORDERTOKEN', new OrderExists(
+ ['state' => [OrderInterface::STATE_NEW, 'other_state']]
+ ));
+ }
+
+ function it_adds_constraint_if_order_does_not_exists(
+ OrderRepositoryInterface $orderRepository,
+ ExecutionContextInterface $executionContext
+ ): void {
+ $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN', 'state' => OrderInterface::STATE_NEW])->willReturn(null);
+
+ $executionContext->addViolation('sylius.shop_api.order.not_exists')->shouldBeCalled();
+
+ $this->validate('ORDERTOKEN', new OrderExists(['state' => OrderInterface::STATE_NEW]));
+ }
+}
diff --git a/src/Command/Order/UpdatePaymentMethod.php b/src/Command/Order/UpdatePaymentMethod.php
new file mode 100644
index 000000000..6301818a6
--- /dev/null
+++ b/src/Command/Order/UpdatePaymentMethod.php
@@ -0,0 +1,41 @@
+orderToken = $orderToken;
+ $this->paymentId = $paymentId;
+ $this->paymentMethodCode = $paymentMethodCode;
+ }
+
+ public function orderToken(): string
+ {
+ return $this->orderToken;
+ }
+
+ /** @return string|int */
+ public function paymentId()
+ {
+ return $this->paymentId;
+ }
+
+ public function paymentMethodCode(): string
+ {
+ return $this->paymentMethodCode;
+ }
+}
diff --git a/src/Controller/Order/UpdatePaymentMethodAction.php b/src/Controller/Order/UpdatePaymentMethodAction.php
new file mode 100644
index 000000000..227342afe
--- /dev/null
+++ b/src/Controller/Order/UpdatePaymentMethodAction.php
@@ -0,0 +1,59 @@
+viewHandler = $viewHandler;
+ $this->bus = $bus;
+ $this->validator = $validator;
+ $this->validationErrorViewFactory = $validationErrorViewFactory;
+ }
+
+ public function __invoke(Request $request): Response
+ {
+ $updateRequest = new UpdatePaymentMethodRequest($request);
+
+ $validationResults = $this->validator->validate($updateRequest);
+ if (0 !== count($validationResults)) {
+ return $this->viewHandler->handle(
+ View::create($this->validationErrorViewFactory->create($validationResults),
+ Response::HTTP_BAD_REQUEST
+ )
+ );
+ }
+
+ $this->bus->dispatch($updateRequest->getCommand());
+
+ return $this->viewHandler->handle(View::create(null, Response::HTTP_NO_CONTENT));
+ }
+}
diff --git a/src/Factory/Order/PlacedOrderViewFactory.php b/src/Factory/Order/PlacedOrderViewFactory.php
index 66d6c869f..00f4f6b6c 100644
--- a/src/Factory/Order/PlacedOrderViewFactory.php
+++ b/src/Factory/Order/PlacedOrderViewFactory.php
@@ -66,6 +66,7 @@ public function create(OrderInterface $order, string $localeCode): PlacedOrderVi
$placedOrderView->locale = $localeCode;
$placedOrderView->checkoutState = $order->getCheckoutState();
$placedOrderView->checkoutCompletedAt = $order->getCheckoutCompletedAt()->format('c');
+ $placedOrderView->paymentState = $order->getPaymentState();
$placedOrderView->totals = $this->totalViewFactory->create($order);
$placedOrderView->tokenValue = $order->getTokenValue();
$placedOrderView->number = $order->getNumber();
diff --git a/src/Handler/Order/UpdatePaymentMethodHandler.php b/src/Handler/Order/UpdatePaymentMethodHandler.php
new file mode 100644
index 000000000..9e50d3ff5
--- /dev/null
+++ b/src/Handler/Order/UpdatePaymentMethodHandler.php
@@ -0,0 +1,52 @@
+orderRepository = $orderRepository;
+ $this->paymentMethodRepository = $paymentMethodRepository;
+ }
+
+ public function __invoke(UpdatePaymentMethod $choosePaymentMethod): void
+ {
+ /** @var OrderInterface $order */
+ $order = $this->orderRepository->findOneBy(['tokenValue' => $choosePaymentMethod->orderToken()]);
+
+ Assert::notNull($order, 'Order has not been found.');
+ Assert::notSame(OrderInterface::STATE_CART, $order->getState(), 'Only orders can be updated.');
+ Assert::same(OrderPaymentStates::STATE_AWAITING_PAYMENT, $order->getPaymentState(), 'Only awaiting payment orders can be updated.');
+
+ /** @var PaymentMethodInterface $paymentMethod */
+ $paymentMethod = $this->paymentMethodRepository->findOneBy(['code' => $choosePaymentMethod->paymentMethodCode()]);
+
+ Assert::notNull($paymentMethod, 'Payment method has not been found');
+ Assert::true(isset($order->getPayments()[$choosePaymentMethod->paymentId()]), 'Can not find payment with given identifier.');
+
+ $payment = $order->getPayments()[$choosePaymentMethod->paymentId()];
+ Assert::same(PaymentInterface::STATE_NEW, $payment->getState(), 'Payment should have new state');
+
+ $payment->setMethod($paymentMethod);
+ }
+}
diff --git a/src/Request/Order/UpdatePaymentMethodRequest.php b/src/Request/Order/UpdatePaymentMethodRequest.php
new file mode 100644
index 000000000..8e801272a
--- /dev/null
+++ b/src/Request/Order/UpdatePaymentMethodRequest.php
@@ -0,0 +1,43 @@
+token = $request->attributes->get('token');
+ $this->paymentIdentifier = $request->attributes->get('paymentId');
+ $this->paymentMethod = $request->request->get('method');
+ }
+
+ public function getCommand(): UpdatePaymentMethod
+ {
+ return new UpdatePaymentMethod($this->token, $this->paymentIdentifier, $this->paymentMethod);
+ }
+
+ public function getOrderToken(): string
+ {
+ return $this->token;
+ }
+
+ /** @return int|string */
+ public function getPaymentId()
+ {
+ return $this->paymentIdentifier;
+ }
+}
diff --git a/src/Resources/config/routing/order.yml b/src/Resources/config/routing/order.yml
index fa2215128..733f768da 100644
--- a/src/Resources/config/routing/order.yml
+++ b/src/Resources/config/routing/order.yml
@@ -9,3 +9,15 @@ sylius_shop_api_order_details:
methods: [GET]
defaults:
_controller: sylius.shop_api_plugin.controller.order.show_order_details_action
+
+sylius_shop_api_order_available_payment_methods:
+ path: /orders/{token}/payment
+ methods: [GET]
+ defaults:
+ _controller: sylius.shop_api_plugin.controller.checkout.show_available_payment_methods_action
+
+sylius_shop_api_order_update_payment_method:
+ path: /orders/{token}/payment/{paymentId}
+ methods: [PUT]
+ defaults:
+ _controller: sylius.shop_api_plugin.controller.order.update_payment_method_action
diff --git a/src/Resources/config/services/actions/order.xml b/src/Resources/config/services/actions/order.xml
index e0d38092c..600cf23ff 100644
--- a/src/Resources/config/services/actions/order.xml
+++ b/src/Resources/config/services/actions/order.xml
@@ -19,5 +19,14 @@
+
+
+
+
+
+
+
diff --git a/src/Resources/config/services/handler/order.xml b/src/Resources/config/services/handler/order.xml
new file mode 100644
index 000000000..b3060fb39
--- /dev/null
+++ b/src/Resources/config/services/handler/order.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/config/services/validators/order.xml b/src/Resources/config/services/validators/order.xml
new file mode 100644
index 000000000..b1e4efe29
--- /dev/null
+++ b/src/Resources/config/services/validators/order.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/config/validation/order/UpdatePaymentMethodRequest.xml b/src/Resources/config/validation/order/UpdatePaymentMethodRequest.xml
new file mode 100644
index 000000000..616ca3e10
--- /dev/null
+++ b/src/Resources/config/validation/order/UpdatePaymentMethodRequest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Validator/Constraints/OrderExists.php b/src/Validator/Constraints/OrderExists.php
new file mode 100644
index 000000000..ad0542477
--- /dev/null
+++ b/src/Validator/Constraints/OrderExists.php
@@ -0,0 +1,23 @@
+orderRepository = $orderRepository;
+ }
+
+ public function validate($token, Constraint $constraint): void
+ {
+ Assert::isInstanceOf($constraint, OrderExists::class);
+
+ if (null === $this->orderRepository->findOneBy(['tokenValue' => $token, 'state' => $constraint->state])) {
+ $this->context->addViolation($constraint->message);
+ }
+ }
+}
diff --git a/src/Validator/Order/PaymentNotPaidValidator.php b/src/Validator/Order/PaymentNotPaidValidator.php
new file mode 100644
index 000000000..1641d8cf9
--- /dev/null
+++ b/src/Validator/Order/PaymentNotPaidValidator.php
@@ -0,0 +1,40 @@
+orderRepository = $orderRepository;
+ }
+
+ public function validate($updatePayment, Constraint $constraint): void
+ {
+ /** @var OrderInterface|null $order */
+ $order = $this->orderRepository->findOneBy(['tokenValue' => $updatePayment->getOrderToken()]);
+ if ($order === null) {
+ return;
+ }
+
+ $payment = $order->getPayments()[$updatePayment->getPaymentId()] ?? null;
+ if ($payment === null) {
+ return;
+ }
+
+ if (!in_array($payment->getState(), [PaymentInterface::STATE_NEW, PaymentInterface::STATE_CANCELLED])) {
+ $this->context->addViolation($constraint->message);
+ }
+ }
+}
diff --git a/src/View/Order/PlacedOrderView.php b/src/View/Order/PlacedOrderView.php
index ae0ee106e..644a79212 100644
--- a/src/View/Order/PlacedOrderView.php
+++ b/src/View/Order/PlacedOrderView.php
@@ -28,6 +28,9 @@ class PlacedOrderView
/** @var string */
public $checkoutCompletedAt;
+ /** @var string */
+ public $paymentState;
+
/** @var array|ItemView[] */
public $items = [];
diff --git a/tests/Controller/Order/OrderUpdatePaymentMethodApiTest.php b/tests/Controller/Order/OrderUpdatePaymentMethodApiTest.php
new file mode 100644
index 000000000..e5ef5fc75
--- /dev/null
+++ b/tests/Controller/Order/OrderUpdatePaymentMethodApiTest.php
@@ -0,0 +1,89 @@
+loadFixturesFromFiles(['customer.yml', 'country.yml', 'address.yml', 'shop.yml', 'payment.yml', 'shipping.yml']);
+ $token = 'ORDERTOKENPLACED';
+ $email = 'oliver@queen.com';
+
+ $this->logInUser($email, '123password');
+
+ $this->placeOrderForCustomerWithEmail($email, $token);
+
+ $data =
+<<client->request('PUT', $this->getPaymentUrl('ORDERTOKENPLACED') . '/0', [], [], self::CONTENT_TYPE_HEADER, $data);
+
+ $response = $this->client->getResponse();
+ $this->assertResponseCode($response, Response::HTTP_NO_CONTENT);
+ }
+
+ /**
+ * @test
+ */
+ public function it_does_not_allow_to_update_payment_method_on_paid_order(): void
+ {
+ $this->loadFixturesFromFiles(['customer.yml', 'country.yml', 'address.yml', 'shop.yml', 'payment.yml', 'shipping.yml']);
+ $token = 'ORDERTOKENPAID';
+ $email = 'oliver@queen.com';
+
+ $this->logInUser($email, '123password');
+
+ $this->placeOrderForCustomerWithEmail($email, $token);
+ $this->markOrderAsPayed($token);
+
+ $data =
+<<client->request('PUT', $this->getPaymentUrl('ORDERTOKENPAID') . '/0', [], [], self::CONTENT_TYPE_HEADER, $data);
+
+ $response = $this->client->getResponse();
+ $this->assertResponseCode($response, Response::HTTP_BAD_REQUEST);
+ }
+
+ private function markOrderAsPayed()
+ {
+ /** @var OrderInterface $order */
+ $order = $this->get('sylius.repository.order')->findAll()[0];
+ foreach ($order->getPayments() as $payment) {
+ $payment->setState(PaymentInterface::STATE_COMPLETED);
+ }
+ $order->setPaymentState(OrderPaymentStates::STATE_PAID);
+
+ $this->get('sylius.manager.order')->flush();
+ }
+
+ private function getPaymentUrl(string $token): string
+ {
+ return sprintf('/shop-api/orders/%s/payment', $token);
+ }
+}
diff --git a/tests/Responses/Expected/order/order_details_response.json b/tests/Responses/Expected/order/order_details_response.json
index ba458f78e..7c233f4eb 100644
--- a/tests/Responses/Expected/order/order_details_response.json
+++ b/tests/Responses/Expected/order/order_details_response.json
@@ -4,6 +4,7 @@
"locale": "en_GB",
"checkoutState": "completed",
"checkoutCompletedAt": "@string@.isDateTime()",
+ "paymentState": "awaiting_payment",
"items": [
{
"id": @integer@,
diff --git a/tests/Responses/Expected/order/order_details_response_guest.json b/tests/Responses/Expected/order/order_details_response_guest.json
index 397c879f8..a1d44cea6 100644
--- a/tests/Responses/Expected/order/order_details_response_guest.json
+++ b/tests/Responses/Expected/order/order_details_response_guest.json
@@ -4,6 +4,7 @@
"locale": "en_GB",
"checkoutState": "completed",
"checkoutCompletedAt": "@string@.isDateTime()",
+ "paymentState": "awaiting_payment",
"items": [
{
"id": @integer@,
diff --git a/tests/Responses/Expected/order/orders_list_response.json b/tests/Responses/Expected/order/orders_list_response.json
index 7b7ba7879..4068a47d7 100644
--- a/tests/Responses/Expected/order/orders_list_response.json
+++ b/tests/Responses/Expected/order/orders_list_response.json
@@ -5,6 +5,7 @@
"locale": "en_GB",
"checkoutState": "completed",
"checkoutCompletedAt": "@string@.isDateTime()",
+ "paymentState": "awaiting_payment",
"items": [
{
"id": @integer@,