Skip to content
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

Open
wants to merge 3 commits into
base: 1.5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions spec/Payment/InstructionResolverSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing strict type declaration

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing final keyword.

Copy link
Member

Choose a reason for hiding this comment

The 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])
;
}
}
122 changes: 122 additions & 0 deletions src/Controller/Order/GetPaymentInstructionAction.php
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
*/
Copy link
Member

Choose a reason for hiding this comment

The 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);
Copy link
Member

Choose a reason for hiding this comment

The 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 (method, type and content), wdyt?

}

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();
}
}
12 changes: 12 additions & 0 deletions src/Payment/Instruction.php
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing final and docblocks in properties... And maybe it should be placed in src/Model? And have private properties set by constructor and getters? :)

{
public $method;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing typehint declarations

public $type;
public $content = [];
}
28 changes: 28 additions & 0 deletions src/Payment/InstructionResolver.php
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
Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And missing final xD

Copy link
Member

Choose a reason for hiding this comment

The 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;
}
}
18 changes: 18 additions & 0 deletions src/Payment/InstructionResolverInterface.php
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;
}
4 changes: 4 additions & 0 deletions src/Resources/config/routing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/Resources/config/routing/order.yml
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
16 changes: 16 additions & 0 deletions src/Resources/config/services/actions/order.xml
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>
1 change: 1 addition & 0 deletions src/Resources/config/services/controllers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<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">
<imports>
<import resource="actions/checkout.xml"/>
<import resource="actions/order.xml"/>
<import resource="actions/cart.xml"/>
<import resource="actions/product.xml"/>
<import resource="actions/customer.xml"/>
Expand Down
110 changes: 110 additions & 0 deletions tests/Controller/OrderPaymentApiTest.php
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);
}
}
Loading