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

Integrated Payment #262

Merged
merged 37 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4dc16e0
PPSYL-98 - Add new checkbox to enable payment integrated
Jibbarth Nov 21, 2024
a98d372
PPSYL-101 - Add is_payplug_live twig function
Jibbarth Nov 22, 2024
5a139d4
PPSYL-86 - Add endpoint to init payment integrated
Jibbarth Nov 25, 2024
341286a
PPSYL-87 - On post select payment, mark order as checkout completed
Jibbarth Nov 26, 2024
d45ef9b
fix(ui): avoid loading select payment assets everywhere
maxperei Nov 27, 2024
34a0052
refacto(payment): slightly js restructure, reindent twig
maxperei Nov 27, 2024
c1122a0
Merge pull request #138 from synolia/feature/PPSYL-98-add-configuration
Jibbarth Nov 27, 2024
e47ac0f
PPSYL-101 - Switch to isTest instead
Jibbarth Nov 27, 2024
ac2265b
PPSYL-87 - Phpstan issues
Jibbarth Nov 27, 2024
45503ac
feat(form): payplug integrated payment fields on select payment step
maxperei Nov 27, 2024
f764a55
chore(parcel): expose assets
maxperei Nov 27, 2024
4829ec4
Merge pull request #142 from synolia/feat/payplug-integrated-payment-…
maxperei Nov 27, 2024
0fb0c59
Merge pull request #139 from synolia/feature/PPSYL-101-is-live-twig-h…
Jibbarth Nov 28, 2024
a0a85e1
PPSYL-86 - Check the payment method is supported for Integrated Payment
Jibbarth Nov 28, 2024
2289b08
Merge pull request #140 from synolia/feature/PPSYL-86-init-integrated…
Jibbarth Nov 28, 2024
35d00fd
Merge pull request #141 from synolia/feature/PPSYL-87-auto-complete-c…
Jibbarth Nov 28, 2024
57f3760
fix(integrated): uncaught missing fields such as save cards and selec…
maxperei Nov 28, 2024
452b3f6
Merge pull request #143 from synolia/fix/select-payment-integrated-un…
Jibbarth Nov 28, 2024
afd896f
fix(form): payplug integrated payment rendering once became an order
maxperei Dec 2, 2024
0b4657c
Merge pull request #144 from synolia/fix/order-payment-integrated
maxperei Dec 2, 2024
fa38f7b
PPSYL-123 - Handle payment fail and retry from sylius_shop_order_show
Jibbarth Dec 2, 2024
04f1c57
Merge pull request #145 from synolia/fix/PPSYL-123-handle-payment-fail
Jibbarth Dec 3, 2024
bd6855e
chore(deps): regular update
maxperei Dec 10, 2024
1988a57
fix(ui): missing assets in order select payment template
maxperei Dec 10, 2024
cd25a5f
fix(js): improve "save card" behavior on toggling payment methods
maxperei Dec 10, 2024
faf5076
Merge pull request #150 from synolia/fix/integrated-payment-overall-b…
Jibbarth Dec 10, 2024
b3c1791
Phpstan issue
Jibbarth Dec 10, 2024
2289cd3
chore(ci): Update upload-artifact version
Jibbarth Dec 10, 2024
5c3346a
chore(ci): require refund-plugin route for testing
Jibbarth Dec 10, 2024
1f0cbba
Merge pull request #151 from synolia/fix/ci
Jibbarth Dec 11, 2024
b01fd60
Merge pull request #259 from synolia/feature/payment-integrated
dmurillo-payplug Dec 12, 2024
5f6f03c
fix(display): [integrated payment] avoid scheme selection to close fi…
maxperei Jan 7, 2025
a14886e
fix(display): [integrated payment] adjust one click size and margin
maxperei Jan 7, 2025
edbc457
fix(display): [integrated payment] add loader during form validation
maxperei Jan 7, 2025
d16b3e4
chore(parcel): expose assets
maxperei Jan 7, 2025
7f47d74
Merge pull request #156 from synolia/fix/integrated-payment-display
Jibbarth Jan 8, 2025
38560fa
Merge pull request #260 from synolia/feature/payment-integrated
dmurillo-payplug Jan 20, 2025
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
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
Loading