Skip to content

Commit

Permalink
Merge pull request #259 from synolia/feature/payment-integrated
Browse files Browse the repository at this point in the history
Feature: integrated payment
  • Loading branch information
dmurillo-payplug authored Dec 12, 2024
2 parents 4d0e75a + 1f0cbba commit b01fd60
Show file tree
Hide file tree
Showing 51 changed files with 1,455 additions and 196 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/sylius.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ jobs:
run: 'vendor/bin/behat --strict --no-interaction -f progress || vendor/bin/behat --strict -vvv --no-interaction --rerun'
if: 'always() && steps.end-of-setup-sylius.outcome == ''success'''
-
uses: actions/upload-artifact@v2.1.4
uses: actions/upload-artifact@v3
if: failure()
with:
name: logs
Expand Down
2 changes: 2 additions & 0 deletions install/Application/config/routes/sylius_refund.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
sylius_refund:
resource: "@SyliusRefundPlugin/Resources/config/routing.yml"
13 changes: 11 additions & 2 deletions rulesets/phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ parameters:

-
message: "#^Cannot call method getFirstModel\\(\\) on mixed\\.$#"
count: 2
count: 3
path: ../src/Action/StatusAction.php

-
Expand All @@ -77,7 +77,7 @@ parameters:

-
message: "#^Parameter \\#1 \\$paymentId of method PayPlug\\\\SyliusPayPlugPlugin\\\\ApiClient\\\\PayPlugApiClientInterface\\:\\:retrieve\\(\\) expects string, mixed given\\.$#"
count: 2
count: 3
path: ../src/Action/StatusAction.php

-
Expand Down Expand Up @@ -439,3 +439,12 @@ parameters:
message: "#^Parameter \\#1 \\$tokenValue of method Sylius\\\\Component\\\\Order\\\\Repository\\\\OrderRepositoryInterface\\:\\:findOneByTokenValue\\(\\) expects string, mixed given\\.$#"
count: 1
path: ../src/Twig/OneySimulationExtension.php
-
message: "#^PHPDoc tag @param for parameter \\$paymentMethodRepository contains generic type Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface\\<Sylius\\\\Component\\\\Core\\\\Model\\\\PaymentMethodInterface\\> but interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface is not generic\\.$#"
count: 1
path: ../src/Controller/IntegratedPaymentController.php

-
message: "#^PHPDoc tag @var for property PayPlug\\\\SyliusPayPlugPlugin\\\\Controller\\\\IntegratedPaymentController\\:\\:\\$paymentMethodRepository contains generic type Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface\\<Sylius\\\\Component\\\\Core\\\\Model\\\\PaymentMethodInterface\\> but interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface is not generic\\.$#"
count: 1
path: ../src/Controller/IntegratedPaymentController.php
6 changes: 6 additions & 0 deletions src/Action/CaptureAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ public function execute($request): void
return;
}

if (PayPlugApiClientInterface::FAILED === ($details['status'] ?? null) &&
PayPlugApiClientInterface::INTEGRATED_PAYMENT_INTEGRATION === ($details['integration'] ?? null)) {
// Do not try to capture a failed integrated payment and do not remove status
return;
}

if (isset($details['status']) && PayPlugApiClientInterface::FAILED === $details['status']) {
// Unset current status to allow to use payplug to change payment method
unset($details['status']);
Expand Down
5 changes: 5 additions & 0 deletions src/Action/StatusAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ public function execute($request): void
$this->paymentNotificationHandler->treat($request->getFirstModel(), $resource, $details);
}

if (PaymentInterface::STATE_PROCESSING === $details['status']) {
$resource = $this->payPlugApiClient->retrieve($details['payment_id']);
$this->paymentNotificationHandler->treat($request->getFirstModel(), $resource, $details);
}

$payment->setDetails($details->getArrayCopy());
$this->markRequestAs($details['status'], $request);
}
Expand Down
11 changes: 11 additions & 0 deletions src/ApiClient/PayPlugApiClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace PayPlug\SyliusPayPlugPlugin\ApiClient;

use Sylius\Component\Core\Model\PaymentMethodInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
use Symfony\Contracts\Cache\CacheInterface;

Expand Down Expand Up @@ -37,4 +38,14 @@ public function create(string $factoryName, ?string $key = null): PayPlugApiClie

return new PayPlugApiClient($key, $factoryName, $this->cache);
}

public function createForPaymentMethod(PaymentMethodInterface $paymentMethod): PayPlugApiClientInterface
{
$gatewayConfig = $paymentMethod->getGatewayConfig() ?? throw new \LogicException('Gateway config not found');

$key = $gatewayConfig->getConfig()['secretKey'];
$factoryName = $gatewayConfig->getFactoryName();

return new PayPlugApiClient($key, $factoryName, $this->cache);
}
}
2 changes: 2 additions & 0 deletions src/ApiClient/PayPlugApiClientInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ interface PayPlugApiClientInterface
{
public const INTERNAL_STATUS_ONE_CLICK = 'one_click';

public const INTEGRATED_PAYMENT_INTEGRATION = 'INTEGRATED_PAYMENT';

public const LIVE_KEY_PREFIX = 'sk_live';

public const TEST_KEY_PREFIX = 'sk_test';
Expand Down
115 changes: 115 additions & 0 deletions src/Controller/IntegratedPaymentController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

declare(strict_types=1);

namespace PayPlug\SyliusPayPlugPlugin\Controller;

use Doctrine\ORM\EntityManagerInterface;
use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory;
use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface;
use PayPlug\SyliusPayPlugPlugin\Creator\PayPlugPaymentDataCreator;
use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory;
use Psr\Log\LoggerInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\PaymentInterface;
use Sylius\Component\Core\Model\PaymentMethodInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Sylius\Component\Order\Context\CartContextInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

final class IntegratedPaymentController extends AbstractController
{
private CartContextInterface $cartContext;
/**
* @var RepositoryInterface<\Sylius\Component\Core\Model\PaymentMethodInterface>
*/
private RepositoryInterface $paymentMethodRepository;
private OrderRepositoryInterface $orderRepository;
private PayPlugPaymentDataCreator $paymentDataCreator;
private PayPlugApiClientFactory $apiClientFactory;
private EntityManagerInterface $entityManager;
private LoggerInterface $logger;

/**
* @param RepositoryInterface<\Sylius\Component\Core\Model\PaymentMethodInterface> $paymentMethodRepository
*/
public function __construct(
CartContextInterface $cartContext,
RepositoryInterface $paymentMethodRepository,
OrderRepositoryInterface $orderRepository,
PayPlugPaymentDataCreator $paymentDataCreator,
PayPlugApiClientFactory $apiClientFactory,
EntityManagerInterface $entityManager,
LoggerInterface $logger
) {
$this->cartContext = $cartContext;
$this->paymentMethodRepository = $paymentMethodRepository;
$this->orderRepository = $orderRepository;
$this->paymentDataCreator = $paymentDataCreator;
$this->apiClientFactory = $apiClientFactory;
$this->entityManager = $entityManager;
$this->logger = $logger;
}

/**
* The InitPayment action is called when the user clicks on the "Pay" button from the integratedPayment iframe.
*
* The actual payment (ie latest in cart state) of the order is sent to Payplug,
* specifying the IntegratedPayment integration.
*
* @see https://docs.payplug.com/api/integratedref.html#trigger-a-payment
*/
public function initPaymentAction(Request $request, int $paymentMethodId): Response
{
$paymentMethod = $this->paymentMethodRepository->find($paymentMethodId);
if (!$paymentMethod instanceof PaymentMethodInterface) {
throw $this->createNotFoundException();
}

$order = null;
if (\is_string($orderToken = $request->query->get('orderToken'))) {
$order = $this->orderRepository->findOneByTokenValue($orderToken);
}
if (null === $order) {
$order = $this->cartContext->getCart();
}
if (!$order instanceof OrderInterface) {
throw $this->createNotFoundException('No order found');
}

$payment = $order->getLastPayment();
if (!$payment instanceof PaymentInterface) {
throw $this->createNotFoundException('No payment available');
}

$payment->setMethod($paymentMethod);
$factoryName = $paymentMethod->getGatewayConfig()?->getFactoryName();
if (PayPlugGatewayFactory::FACTORY_NAME !== $factoryName) {
throw new BadRequestHttpException('Unsupported payment method of Integrated Payment');
}

$paymentData = $this->paymentDataCreator->create($payment, $factoryName);
// Mandatory
$paymentData['integration'] = PayPlugApiClientInterface::INTEGRATED_PAYMENT_INTEGRATION;
$this->logger->debug('Payplug Payment data for creation', $paymentData->getArrayCopy());

$apiClient = $this->apiClientFactory->create($factoryName);
$payplugPayment = $apiClient->createPayment($paymentData->getArrayCopy());
$this->logger->debug('PayPlug payment created', (array) $payplugPayment);

$paymentData['payment_id'] = $payplugPayment->id;
$paymentData['is_live'] = $payplugPayment->is_live;
$payment->setDetails($paymentData->getArrayCopy());

$this->entityManager->flush();

return new JsonResponse([
'payment_id' => $payplugPayment->id,
], Response::HTTP_CREATED);
}
}
140 changes: 140 additions & 0 deletions src/EventSubscriber/PostPaymentSelectEventSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php

declare(strict_types=1);

namespace PayPlug\SyliusPayPlugPlugin\EventSubscriber;

use Doctrine\ORM\EntityManagerInterface;
use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface;
use SM\Factory\FactoryInterface;
use Sylius\Bundle\ResourceBundle\Event\ResourceControllerEvent;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\PaymentInterface;
use Sylius\Component\Core\OrderCheckoutTransitions;
use Sylius\Component\Resource\Model\ResourceInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Webmozart\Assert\Assert;

final class PostPaymentSelectEventSubscriber implements EventSubscriberInterface
{
private const CHECKOUT_ROUTE = 'sylius_shop_checkout_select_payment';
private const UPDATE_ORDER_PAYMENT_ROUTE = 'sylius_shop_order_show';

private const TOKEN_FIELD = 'payplug_integrated_payment_token';

public function __construct(
private RequestStack $requestStack,
private EntityManagerInterface $entityManager,
private FactoryInterface $stateMachineFactory,
) {
}

public static function getSubscribedEvents(): array
{
return [
RequestEvent::class => 'alterRequestConfigurationForIntegratedPayment',
'sylius.order.post_payment' => 'handle',
'sylius.order.post_update' => 'handle',
];
}

public function alterRequestConfigurationForIntegratedPayment(RequestEvent $event): void
{
$request = $event->getRequest();
if (!$this->hasToken($request) || self::CHECKOUT_ROUTE !== $request->attributes->get('_route')) {
return;
}
if (!$request->attributes->has('_sylius')) {
return;
}

$syliusRequestConfig = $request->attributes->get('_sylius');
if (!\is_array($syliusRequestConfig)) {
return;
}

$syliusRequestConfig['redirect'] = [
'route' => 'sylius_shop_order_pay',
'parameters' => ['tokenValue' => 'resource.tokenValue'],
];

$request->attributes->set('_sylius', $syliusRequestConfig);
}
public function handle(ResourceControllerEvent $resourceControllerEvent): void
{
$request = $this->requestStack->getCurrentRequest();
if (null === $request) {
return;
}

if (!\in_array($request->attributes->get('_route'), [self::CHECKOUT_ROUTE, self::UPDATE_ORDER_PAYMENT_ROUTE], true)) {
return;
}

/** @var \Sylius\Component\Core\Model\OrderInterface $order */
$order = $resourceControllerEvent->getSubject();
$lastPayment = $order->getLastPayment();
if (null === $lastPayment) {
return;
}

if (!$this->hasToken($request)) {
return;

}
$this->handleToken($resourceControllerEvent, $request, $lastPayment);
}

private function handleToken(
ResourceControllerEvent $resourceControllerEvent,
Request $request,
PaymentInterface $lastPayment,
): void {
$token = $this->getToken($request);

$lastPayment->setDetails(\array_merge(
$lastPayment->getDetails(),
[
'payment_id' => $token,
'status' => PaymentInterface::STATE_PROCESSING,
]
));

$resource = $resourceControllerEvent->getSubject();
Assert::isInstanceOf($resource, ResourceInterface::class);

$this->applyToComplete($lastPayment->getOrder() ?? throw new \LogicException('Order not found for payment'));
}

private function hasToken(Request $request): bool
{
if (!$request->request->has(self::TOKEN_FIELD)) {
return false;
}

$token = $this->getToken($request);

return '' !== $token;
}

private function getToken(Request $request): string
{
$token = $request->request->get(self::TOKEN_FIELD);
Assert::string($token);

return $token;
}

private function applyToComplete(OrderInterface $order): void
{
$stateMachine = $this->stateMachineFactory->get($order, OrderCheckoutTransitions::GRAPH);
if ($stateMachine->can(OrderCheckoutTransitions::TRANSITION_COMPLETE)) {
$stateMachine->apply(OrderCheckoutTransitions::TRANSITION_COMPLETE);
}

$this->entityManager->flush();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'help_html' => true,
'required' => false,
])
->add(PayPlugGatewayFactory::INTEGRATED_PAYMENT, CheckboxType::class, [
'label' => 'payplug_sylius_payplug_plugin.form.integrated_payment_enable',
'validation_groups' => AbstractGatewayConfigurationType::VALIDATION_GROUPS,
'required' => false,
])
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void {
$data = $event->getData();
// phpstan check
Expand Down
3 changes: 2 additions & 1 deletion src/Gateway/PayPlugGatewayFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
final class PayPlugGatewayFactory extends AbstractGatewayFactory
{
public const FACTORY_NAME = 'payplug';

public const FACTORY_TITLE = 'PayPlug';

// Custom gateway configuration keys
public const ONE_CLICK = 'oneClick';
public const INTEGRATED_PAYMENT = 'integratedPayment';

public const AUTHORIZED_CURRENCIES = [
'EUR' => [
Expand Down
6 changes: 6 additions & 0 deletions src/Resources/config/routing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,9 @@ payplug_shop_checkout_apple_cancel:
method: find
arguments:
- "expr:service('payplug.apple_pay_order.provider').getCurrentCart()"

payplug_sylius_integrated_payment_init:
path: /{_locale}/payplug/integrated_payment/init/{paymentMethodId}
methods: ['GET', 'POST']
defaults:
_controller: 'PayPlug\SyliusPayPlugPlugin\Controller\IntegratedPaymentController::initPaymentAction'
Loading

0 comments on commit b01fd60

Please sign in to comment.