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 new file mode 100644 index 000000000..9c0d4252b --- /dev/null +++ b/src/Controller/Order/GetPaymentInstructionAction.php @@ -0,0 +1,122 @@ +payum = $payum; + $this->orderRepository = $orderRepository; + $this->viewHandler = $viewHandler; + } + + 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); + + 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' => 'redirect', + 'content' => [ + 'url' => $token->getTargetUrl(), + ] + ]); + + 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/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 @@ + + + + + + + + + + + + + + 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..8cfe4a530 --- /dev/null +++ b/tests/Controller/OrderPaymentApiTest.php @@ -0,0 +1,110 @@ +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); + } + + /** + * @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 5e016ea6b..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: @@ -28,4 +41,10 @@ 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" + paypal_translation: + name: 'PayPal Express Checkout' + locale: 'en_GB' + description: + translatable: "@pay_by_paypal" 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." +} 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" + } +}