From 615a85459633b90be46eefb0ead5b35be9ab5ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20J=C4=99drzejewski?= Date: Mon, 24 Sep 2018 19:24:02 +0200 Subject: [PATCH 1/3] Initial implementation of the payment endpoints --- .../Order/GetPaymentInstructionAction.php | 111 ++++++++++++++++++ src/Resources/config/routing.yml | 4 + src/Resources/config/routing/order.yml | 5 + .../config/services/actions/order.xml | 16 +++ src/Resources/config/services/controllers.xml | 1 + tests/Controller/OrderPaymentApiTest.php | 64 ++++++++++ tests/DataFixtures/ORM/payment.yml | 1 + .../order/payment_instruction_offline.json | 5 + 8 files changed, 207 insertions(+) create mode 100644 src/Controller/Order/GetPaymentInstructionAction.php create mode 100644 src/Resources/config/routing/order.yml create mode 100644 src/Resources/config/services/actions/order.xml create mode 100644 tests/Controller/OrderPaymentApiTest.php create mode 100644 tests/Responses/Expected/order/payment_instruction_offline.json diff --git a/src/Controller/Order/GetPaymentInstructionAction.php b/src/Controller/Order/GetPaymentInstructionAction.php new file mode 100644 index 000000000..53c5808bf --- /dev/null +++ b/src/Controller/Order/GetPaymentInstructionAction.php @@ -0,0 +1,111 @@ +payum = $payum; + $this->orderRepository = $orderRepository; + $this->viewHandler = $viewHandler; + $this->getStatusRequestFactory = $getStatusFactory; + } + + public function __invoke(Request $request): Response + { + $token = $request->attributes->get('token'); + + /** @var OrderInterface $order */ + $order = $this->orderRepository->findOneByTokenValue($token); + + if (null === $order) { + throw new NotFoundHttpException(sprintf('Order with token "%s" does not exist.', $token)); + } + + $payment = $order->getLastPayment(PaymentInterface::STATE_NEW); + + if (null === $payment) { + throw new \LogicException(sprintf('Order with token "%s" does not have any "new" payments.', $token)); + } + + $method = $payment->getMethod(); + $gatewayConfig = $method->getGatewayConfig(); + + $token = $this->provideTokenBasedOnPayment($payment); + $view = View::create([ + 'method' => $gatewayConfig->getGatewayName(), + 'type' => 'text', + 'content' => $method->getInstructions(), + ]); + + return $this->viewHandler->handle($view); + } + + private function provideTokenBasedOnPayment(PaymentInterface $payment): TokenInterface + { + $method = $payment->getMethod(); + $gatewayConfig = $method->getGatewayConfig(); + + $token = $this->getTokenFactory()->createCaptureToken( + $gatewayConfig->getGatewayName(), + $payment, + 'sylius_shop_homepage' + ); + + return $token; + } + + private function getTokenFactory(): GenericTokenFactoryInterface + { + return $this->payum->getTokenFactory(); + } +} diff --git a/src/Resources/config/routing.yml b/src/Resources/config/routing.yml index 4c74238fb..00e464f17 100644 --- a/src/Resources/config/routing.yml +++ b/src/Resources/config/routing.yml @@ -22,6 +22,10 @@ sylius_shop_api_checkout: resource: "@ShopApiPlugin/Resources/config/routing/checkout.yml" prefix: /shop-api/checkout +sylius_shop_api_order: + resource: "@ShopApiPlugin/Resources/config/routing/order.yml" + prefix: /shop-api/order + sylius_shop_api_customer: resource: "@ShopApiPlugin/Resources/config/routing/customer.yml" prefix: /shop-api diff --git a/src/Resources/config/routing/order.yml b/src/Resources/config/routing/order.yml new file mode 100644 index 000000000..1c2460e34 --- /dev/null +++ b/src/Resources/config/routing/order.yml @@ -0,0 +1,5 @@ +sylius_shop_api_order_payment_instruction: + path: /{token}/payment-instruction + methods: [GET] + defaults: + _controller: sylius.shop_api_plugin.controller.order.payment_instruction_action diff --git a/src/Resources/config/services/actions/order.xml b/src/Resources/config/services/actions/order.xml new file mode 100644 index 000000000..acaf239c4 --- /dev/null +++ b/src/Resources/config/services/actions/order.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/controllers.xml b/src/Resources/config/services/controllers.xml index 4b549cd68..90721aedf 100644 --- a/src/Resources/config/services/controllers.xml +++ b/src/Resources/config/services/controllers.xml @@ -3,6 +3,7 @@ + diff --git a/tests/Controller/OrderPaymentApiTest.php b/tests/Controller/OrderPaymentApiTest.php new file mode 100644 index 000000000..e53fb7254 --- /dev/null +++ b/tests/Controller/OrderPaymentApiTest.php @@ -0,0 +1,64 @@ +loadFixturesFromFiles(['shop.yml', 'country.yml', 'shipping.yml', 'payment.yml']); + + $token = 'SDAOSLEFNWU35H3QLI5325'; + + /** @var CommandBus $bus */ + $bus = $this->get('tactician.commandbus'); + $bus->handle(new PickupCart($token, 'WEB_GB')); + $bus->handle(new PutSimpleItemToCart($token, 'LOGAN_MUG_CODE', 5)); + $bus->handle(new AddressOrder( + $token, + Address::createFromArray([ + 'firstName' => 'Sherlock', + 'lastName' => 'Holmes', + 'city' => 'London', + 'street' => 'Baker Street 221b', + 'countryCode' => 'GB', + 'postcode' => 'NWB', + 'provinceName' => 'Greater London', + ]), Address::createFromArray([ + 'firstName' => 'Sherlock', + 'lastName' => 'Holmes', + 'city' => 'London', + 'street' => 'Baker Street 221b', + 'countryCode' => 'GB', + 'postcode' => 'NWB', + 'provinceName' => 'Greater London', + ]) + )); + + $bus->handle(new ChooseShippingMethod($token, 0, 'DHL')); + $bus->handle(new ChoosePaymentMethod($token, 0, 'PBC')); + $bus->handle(new CompleteOrder($token, "shop@example.com")); + + $this->client->request('GET', sprintf('/shop-api/order/%s/payment-instruction', $token), [], [], [ + 'ACCEPT' => 'application/json', + ]); + + $response = $this->client->getResponse(); + $this->assertResponse($response, 'order/payment_instruction_offline', Response::HTTP_OK); + } +} diff --git a/tests/DataFixtures/ORM/payment.yml b/tests/DataFixtures/ORM/payment.yml index 5e016ea6b..0216c4abb 100644 --- a/tests/DataFixtures/ORM/payment.yml +++ b/tests/DataFixtures/ORM/payment.yml @@ -28,4 +28,5 @@ Sylius\Component\Payment\Model\PaymentMethodTranslation: name: 'Pay by check' locale: 'en_GB' description: + instructions: "Please make bank transfer to: PL1234 1234 1234 1234." translatable: "@pay_by_check" diff --git a/tests/Responses/Expected/order/payment_instruction_offline.json b/tests/Responses/Expected/order/payment_instruction_offline.json new file mode 100644 index 000000000..9a08db5c9 --- /dev/null +++ b/tests/Responses/Expected/order/payment_instruction_offline.json @@ -0,0 +1,5 @@ +{ + "method": "Offline", + "type": "text", + "content": "Please make bank transfer to: PL1234 1234 1234 1234." +} From 1410dd419cd4406477d4262e2b71b94089bb7df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20J=C4=99drzejewski?= Date: Mon, 24 Sep 2018 19:53:07 +0200 Subject: [PATCH 2/3] Implement redirect option for Hosted Payment Pages --- .../Order/GetPaymentInstructionAction.php | 22 ++++++++- tests/Controller/OrderPaymentApiTest.php | 48 ++++++++++++++++++- tests/DataFixtures/ORM/payment.yml | 18 +++++++ .../order/payment_instruction_paypal.json | 7 +++ 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 tests/Responses/Expected/order/payment_instruction_paypal.json diff --git a/src/Controller/Order/GetPaymentInstructionAction.php b/src/Controller/Order/GetPaymentInstructionAction.php index 53c5808bf..0dda6511b 100644 --- a/src/Controller/Order/GetPaymentInstructionAction.php +++ b/src/Controller/Order/GetPaymentInstructionAction.php @@ -9,6 +9,7 @@ use League\Tactician\CommandBus; use Payum\Core\Model\GatewayConfigInterface; use Payum\Core\Payum; +use Payum\Core\Request\Capture; use Payum\Core\Request\Generic; use Payum\Core\Request\GetStatusInterface; use Payum\Core\Security\GenericTokenFactoryInterface; @@ -81,10 +82,27 @@ public function __invoke(Request $request): Response $gatewayConfig = $method->getGatewayConfig(); $token = $this->provideTokenBasedOnPayment($payment); + + if ('offline' === $gatewayConfig->getFactoryName()) { + $view = View::create([ + 'method' => $gatewayConfig->getGatewayName(), + 'type' => 'text', + 'content' => $method->getInstructions(), + ]); + + return $this->viewHandler->handle($view); + } + + $gateway = $this->payum->getGateway($token->getGatewayName()); + $gateway->execute(new Capture($token)); + $this->payum->getHttpRequestVerifier()->invalidate($token); + $view = View::create([ 'method' => $gatewayConfig->getGatewayName(), - 'type' => 'text', - 'content' => $method->getInstructions(), + 'type' => 'redirect', + 'content' => [ + 'url' => $token->getTargetUrl(), + ] ]); return $this->viewHandler->handle($view); diff --git a/tests/Controller/OrderPaymentApiTest.php b/tests/Controller/OrderPaymentApiTest.php index e53fb7254..8cfe4a530 100644 --- a/tests/Controller/OrderPaymentApiTest.php +++ b/tests/Controller/OrderPaymentApiTest.php @@ -19,7 +19,7 @@ final class OrderPaymentApiTest extends JsonApiTestCase /** * @test */ - public function it_displays_payment_instructions_for_offline_payment() + public function it_returns_text_payment_instructions_for_offline_payment() { $this->loadFixturesFromFiles(['shop.yml', 'country.yml', 'shipping.yml', 'payment.yml']); @@ -61,4 +61,50 @@ public function it_displays_payment_instructions_for_offline_payment() $response = $this->client->getResponse(); $this->assertResponse($response, 'order/payment_instruction_offline', Response::HTTP_OK); } + + /** + * @test + */ + public function it_returns_redirect_instruction_for_hosted_payment_pages() + { + $this->loadFixturesFromFiles(['shop.yml', 'country.yml', 'shipping.yml', 'payment.yml']); + + $token = 'SDAOSLEFNWU35H3QLI5325'; + + /** @var CommandBus $bus */ + $bus = $this->get('tactician.commandbus'); + $bus->handle(new PickupCart($token, 'WEB_GB')); + $bus->handle(new PutSimpleItemToCart($token, 'LOGAN_MUG_CODE', 5)); + $bus->handle(new AddressOrder( + $token, + Address::createFromArray([ + 'firstName' => 'Sherlock', + 'lastName' => 'Holmes', + 'city' => 'London', + 'street' => 'Baker Street 221b', + 'countryCode' => 'GB', + 'postcode' => 'NWB', + 'provinceName' => 'Greater London', + ]), Address::createFromArray([ + 'firstName' => 'Sherlock', + 'lastName' => 'Holmes', + 'city' => 'London', + 'street' => 'Baker Street 221b', + 'countryCode' => 'GB', + 'postcode' => 'NWB', + 'provinceName' => 'Greater London', + ]) + )); + + $bus->handle(new ChooseShippingMethod($token, 0, 'DHL')); + $bus->handle(new ChoosePaymentMethod($token, 0, 'paypal')); + $bus->handle(new CompleteOrder($token, "shop@example.com")); + + $this->client->request('GET', sprintf('/shop-api/order/%s/payment-instruction', $token), [], [], [ + 'ACCEPT' => 'application/json', + ]); + + $response = $this->client->getResponse(); + $this->assertResponse($response, 'order/payment_instruction_paypal', Response::HTTP_OK); + } } diff --git a/tests/DataFixtures/ORM/payment.yml b/tests/DataFixtures/ORM/payment.yml index 0216c4abb..fd6d89d4c 100644 --- a/tests/DataFixtures/ORM/payment.yml +++ b/tests/DataFixtures/ORM/payment.yml @@ -11,12 +11,25 @@ Sylius\Component\Core\Model\PaymentMethod: gatewayConfig: '@offline' currentLocale: "en_GB" channels: ["@gb_web_channel"] + pay_by_paypal: + code: "paypal" + enabled: true + gatewayConfig: '@paypal' + currentLocale: "en_GB" + channels: ["@gb_web_channel"] Sylius\Bundle\PayumBundle\Model\GatewayConfig: offline: gatewayName: 'Offline' factoryName: 'offline' config: [] + paypal: + gatewayName: 'PayPal Express Checkout' + factoryName: 'paypal_express_checkout' + config: + username: "test" + password: "test" + signature: "test" Sylius\Component\Payment\Model\PaymentMethodTranslation: cash_on_delivery_translation: @@ -30,3 +43,8 @@ Sylius\Component\Payment\Model\PaymentMethodTranslation: description: instructions: "Please make bank transfer to: PL1234 1234 1234 1234." translatable: "@pay_by_check" + paypal_translation: + name: 'PayPal Express Checkout' + locale: 'en_GB' + description: + translatable: "@pay_by_paypal" diff --git a/tests/Responses/Expected/order/payment_instruction_paypal.json b/tests/Responses/Expected/order/payment_instruction_paypal.json new file mode 100644 index 000000000..b0b396451 --- /dev/null +++ b/tests/Responses/Expected/order/payment_instruction_paypal.json @@ -0,0 +1,7 @@ +{ + "method": "PayPal Express Checkout", + "type": "redirect", + "content": { + "url": "https://xxx" + } +} From 2273f944aa1a7420db196edaba96708cbf0730fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20J=C4=99drzejewski?= Date: Mon, 24 Sep 2018 22:55:48 +0200 Subject: [PATCH 3/3] Initial implementation of the instruction resolving service --- spec/Payment/InstructionResolverSpec.php | 54 +++++++++++++++++++ .../Order/GetPaymentInstructionAction.php | 9 +--- src/Payment/Instruction.php | 12 +++++ src/Payment/InstructionResolver.php | 28 ++++++++++ src/Payment/InstructionResolverInterface.php | 18 +++++++ 5 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 spec/Payment/InstructionResolverSpec.php create mode 100644 src/Payment/Instruction.php create mode 100644 src/Payment/InstructionResolver.php create mode 100644 src/Payment/InstructionResolverInterface.php diff --git a/spec/Payment/InstructionResolverSpec.php b/spec/Payment/InstructionResolverSpec.php new file mode 100644 index 000000000..825f59cf9 --- /dev/null +++ b/spec/Payment/InstructionResolverSpec.php @@ -0,0 +1,54 @@ +shouldHaveType(InstructionResolver::class); + } + + function it_implements_instruction_resolver_interface() + { + $this->shouldImplement(InstructionResolverInterface::class); + } + + function it_returns_text_content_for_offline_payments( + PaymentInterface $payment, + PaymentMethodInterface $method, + GatewayConfigInterface $gatewayConfig + ) { + $payment->getMethod()->willReturn($method); + $method->getGatewayConfig()->willReturn($gatewayConfig); + $method->getInstructions()->willReturn('Please make bank transfer to PL1234 1234 1234 1234.'); + $gatewayConfig->getFactoryName()->willReturn(InstructionResolverInterface::GATEWAY_OFFLINE); + + $expectedInstruction = new Instruction(); + $expectedInstruction->gateway = InstructionResolverInterface::GATEWAY_OFFLINE; + $expectedInstruction->type = InstructionResolverInterface::TYPE_TEXT; + $expectedInstruction->content = 'Please make bank transfer to PL1234 1234 1234 1234.'; + + $this->getInstruction($payment)->shouldBeLike($expectedInstruction); + } + + function it_throws_an_exception_when_method_is_not_set( + PaymentInterface $payment + ) { + $payment->getMethod()->willReturn(null); + + $this + ->shouldThrow(new \InvalidArgumentException('Payment method is not set.')) + ->during('getInstruction', [$payment]) + ; + } +} diff --git a/src/Controller/Order/GetPaymentInstructionAction.php b/src/Controller/Order/GetPaymentInstructionAction.php index 0dda6511b..9c0d4252b 100644 --- a/src/Controller/Order/GetPaymentInstructionAction.php +++ b/src/Controller/Order/GetPaymentInstructionAction.php @@ -11,11 +11,9 @@ use Payum\Core\Payum; use Payum\Core\Request\Capture; use Payum\Core\Request\Generic; -use Payum\Core\Request\GetStatusInterface; use Payum\Core\Security\GenericTokenFactoryInterface; use Payum\Core\Security\HttpRequestVerifierInterface; use Payum\Core\Security\TokenInterface; -use Sylius\Bundle\PayumBundle\Factory\GetStatusFactoryInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; @@ -46,19 +44,14 @@ final class GetPaymentInstructionAction */ private $viewHandler; - /** @var GetStatusFactoryInterface */ - private $getStatusRequestFactory; - public function __construct( Payum $payum, OrderRepositoryInterface $orderRepository, - ViewHandlerInterface $viewHandler, - GetStatusFactoryInterface $getStatusFactory + ViewHandlerInterface $viewHandler ) { $this->payum = $payum; $this->orderRepository = $orderRepository; $this->viewHandler = $viewHandler; - $this->getStatusRequestFactory = $getStatusFactory; } public function __invoke(Request $request): Response diff --git a/src/Payment/Instruction.php b/src/Payment/Instruction.php new file mode 100644 index 000000000..c4520bed4 --- /dev/null +++ b/src/Payment/Instruction.php @@ -0,0 +1,12 @@ +getMethod(); + + if (null === $method) { + throw new \InvalidArgumentException('Payment method is not set.'); + } + + $gatewayConfig = $method->getGatewayConfig(); + + $instruction = new Instruction(); + $instruction->gateway = InstructionResolverInterface::GATEWAY_OFFLINE; + $instruction->type = InstructionResolverInterface::TYPE_TEXT; + $instruction->content = $method->getInstructions(); + + return $instruction; + } +} diff --git a/src/Payment/InstructionResolverInterface.php b/src/Payment/InstructionResolverInterface.php new file mode 100644 index 000000000..0826f7462 --- /dev/null +++ b/src/Payment/InstructionResolverInterface.php @@ -0,0 +1,18 @@ +