-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Payments #310
base: 1.5
Are you sure you want to change the base?
[WIP] Payments #310
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
<?php | ||
|
||
namespace spec\Sylius\ShopApiPlugin\Payment; | ||
|
||
use Payum\Core\Model\GatewayConfigInterface; | ||
use PhpSpec\ObjectBehavior; | ||
use Prophecy\Argument; | ||
use Sylius\Component\Core\Model\PaymentInterface; | ||
use Sylius\Component\Core\Model\PaymentMethodInterface; | ||
use Sylius\ShopApiPlugin\Payment\Instruction; | ||
use Sylius\ShopApiPlugin\Payment\InstructionResolver; | ||
use Sylius\ShopApiPlugin\Payment\InstructionResolverInterface; | ||
|
||
class InstructionResolverSpec extends ObjectBehavior | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. final |
||
{ | ||
function it_is_initializable() | ||
{ | ||
$this->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]) | ||
; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Sylius\ShopApiPlugin\Controller\Order; | ||
|
||
use FOS\RestBundle\View\View; | ||
use FOS\RestBundle\View\ViewHandlerInterface; | ||
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\Security\GenericTokenFactoryInterface; | ||
use Payum\Core\Security\HttpRequestVerifierInterface; | ||
use Payum\Core\Security\TokenInterface; | ||
use Sylius\Component\Core\Model\OrderInterface; | ||
use Sylius\Component\Core\Model\PaymentInterface; | ||
use Sylius\Component\Core\Model\PaymentMethodInterface; | ||
use Sylius\Component\Order\Repository\OrderRepositoryInterface; | ||
use Sylius\ShopApiPlugin\Factory\ValidationErrorViewFactoryInterface; | ||
use Sylius\ShopApiPlugin\Request\RegisterCustomerRequest; | ||
use Symfony\Component\HttpFoundation\RedirectResponse; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; | ||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; | ||
use Symfony\Component\Validator\Validator\ValidatorInterface; | ||
|
||
final class GetPaymentInstructionAction | ||
{ | ||
/** | ||
* @var Payum | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I see, there is a convention kept in this plugin with short docblocks, so it should be /** @var Payum */
private $payum; The same for the rest of properties |
||
private $payum; | ||
|
||
/** | ||
* @var OrderRepositoryInterface | ||
*/ | ||
private $orderRepository; | ||
|
||
/** | ||
* @var ViewHandlerInterface | ||
*/ | ||
private $viewHandler; | ||
|
||
public function __construct( | ||
Payum $payum, | ||
OrderRepositoryInterface $orderRepository, | ||
ViewHandlerInterface $viewHandler | ||
) { | ||
$this->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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a lot of logic in this action :( Maybe we could extract some service/services to handle it? Here we could only get the token from the request, pass it to the service that would return some kind of VO containing data required to build a 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(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Sylius\ShopApiPlugin\Payment; | ||
|
||
class Instruction | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing |
||
{ | ||
public $method; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing typehint declarations |
||
public $type; | ||
public $content = []; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Sylius\ShopApiPlugin\Payment; | ||
|
||
use Sylius\Component\Core\Model\PaymentInterface; | ||
|
||
class InstructionResolver implements InstructionResolverInterface | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I see, so there are some services, but not used in the action 😄 We should use them definitely There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And missing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this class used anywhere? |
||
{ | ||
final public function getInstruction(PaymentInterface $payment) : Instruction | ||
{ | ||
$method = $payment->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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Sylius\ShopApiPlugin\Payment; | ||
|
||
use Sylius\Component\Core\Model\PaymentInterface; | ||
|
||
interface InstructionResolverInterface | ||
{ | ||
const GATEWAY_OFFLINE = 'offline'; | ||
const GATEWAY_HPP = 'hosted_payment_page'; | ||
|
||
const TYPE_TEXT = 'text'; | ||
const TYPE_REDIRECT = 'redirect'; | ||
|
||
public function getInstruction(PaymentInterface $payment) : Instruction; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
|
||
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> | ||
<services> | ||
<defaults public="true" /> | ||
|
||
<service id="sylius.shop_api_plugin.controller.order.payment_instruction_action" | ||
class="Sylius\ShopApiPlugin\Controller\Order\GetPaymentInstructionAction" | ||
> | ||
<argument type="service" id="payum" /> | ||
<argument type="service" id="sylius.repository.order" /> | ||
<argument type="service" id="fos_rest.view_handler" /> | ||
<argument type="service" id="sylius.factory.payum_get_status_action" /> | ||
</service> | ||
</services> | ||
</container> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Tests\Sylius\ShopApiPlugin\Controller; | ||
|
||
use League\Tactician\CommandBus; | ||
use Sylius\ShopApiPlugin\Command\AddressOrder; | ||
use Sylius\ShopApiPlugin\Command\ChoosePaymentMethod; | ||
use Sylius\ShopApiPlugin\Command\ChooseShippingMethod; | ||
use Sylius\ShopApiPlugin\Command\PickupCart; | ||
use Sylius\ShopApiPlugin\Command\PutSimpleItemToCart; | ||
use Sylius\ShopApiPlugin\Command\CompleteOrder; | ||
use Sylius\ShopApiPlugin\Model\Address; | ||
use Symfony\Component\HttpFoundation\Response; | ||
|
||
final class OrderPaymentApiTest extends JsonApiTestCase | ||
{ | ||
/** | ||
* @test | ||
*/ | ||
public function it_returns_text_payment_instructions_for_offline_payment() | ||
{ | ||
$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, 'PBC')); | ||
$bus->handle(new CompleteOrder($token, "[email protected]")); | ||
|
||
$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, "[email protected]")); | ||
|
||
$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); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing strict type declaration