Skip to content

Commit

Permalink
feature #106 Order refund webhook fix (SirDomin)
Browse files Browse the repository at this point in the history
This PR was merged into the 1.0-dev branch.

Discussion
----------

paypal doesnt send order id in webhook, we need to make calls to get order payment and then order with its id

Based on #105 

Commits
-------

ca0e8db handle order refund webhook fixed
8cebe3a getting order data from webhook id
55740fa pr-fix
  • Loading branch information
Zales0123 authored Sep 22, 2020
2 parents caf5316 + 55740fa commit 7c77e21
Show file tree
Hide file tree
Showing 19 changed files with 314 additions and 49 deletions.
1 change: 1 addition & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<file name="src/Payum/Action/CompleteOrderAction.php"/>
<file name="src/Processor/PayPalPaymentRefundProcessor.php"/>
<file name="src/Controller/UpdatePayPalOrderAction.php"/>
<file name="src/Controller/Webhook/RefundOrderAction.php"/>
</errorLevel>
</MixedArrayAccess>

Expand Down
40 changes: 0 additions & 40 deletions spec/Processor/PayPalPaymentRefundProcessorSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,46 +121,6 @@ function it_does_nothing_if_payment_is_payment_has_not_pay_pal_order_id(
$this->refund($payment);
}

function it_throws_exception_if_refund_could_not_be_processed(
CacheAuthorizeClientApiInterface $authorizeClientApi,
OrderDetailsApiInterface $orderDetailsApi,
RefundPaymentApiInterface $refundOrderApi,
PayPalAuthAssertionGeneratorInterface $payPalAuthAssertionGenerator,
RefundReferenceNumberProviderInterface $refundReferenceNumberProvider,
PaymentInterface $payment,
PaymentMethodInterface $paymentMethod,
GatewayConfigInterface $gatewayConfig,
OrderInterface $order
): void {
$payment->getMethod()->willReturn($paymentMethod);
$paymentMethod->getGatewayConfig()->willReturn($gatewayConfig);
$gatewayConfig->getFactoryName()->willReturn('sylius.pay_pal');
$payment->getDetails()->willReturn(['paypal_order_id' => '123123']);

$authorizeClientApi->authorize($paymentMethod)->willReturn('TOKEN');
$orderDetailsApi
->get('TOKEN', '123123')
->willReturn(['purchase_units' => [['payments' => ['captures' => [['id' => '555', 'status' => 'COMPLETED']]]]]])
;
$payPalAuthAssertionGenerator->generate($paymentMethod)->willReturn('AUTH-ASSERTION');

$payment->getAmount()->willReturn(1000);
$payment->getOrder()->willReturn($order);
$order->getCurrencyCode()->willReturn('USD');

$refundReferenceNumberProvider->provide($payment)->willReturn('REFERENCE-NUMBER');

$refundOrderApi
->refund('TOKEN', '555', 'AUTH-ASSERTION', 'REFERENCE-NUMBER', '10.00', 'USD')
->willReturn(['status' => 'FAILED'])
;

$this
->shouldThrow(PayPalOrderRefundException::class)
->during('refund', [$payment])
;
}

function it_throws_exception_if_something_went_wrong_during_refunding_payment(
CacheAuthorizeClientApiInterface $authorizeClientApi,
OrderDetailsApiInterface $orderDetailsApi,
Expand Down
65 changes: 65 additions & 0 deletions spec/Provider/PayPalRefundDataProviderSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace spec\Sylius\PayPalPlugin\Provider;

use PhpSpec\ObjectBehavior;
use Sylius\Component\Core\Model\PaymentMethodInterface;
use Sylius\PayPalPlugin\Api\CacheAuthorizeClientApiInterface;
use Sylius\PayPalPlugin\Api\GenericApiInterface;
use Sylius\PayPalPlugin\Exception\PayPalWrongDataException;
use Sylius\PayPalPlugin\Provider\PayPalPaymentMethodProviderInterface;

final class PayPalRefundDataProviderSpec extends ObjectBehavior
{
public function let(
CacheAuthorizeClientApiInterface $authorizeClientApi,
GenericApiInterface $genericApi,
PayPalPaymentMethodProviderInterface $payPalPaymentMethodProvider
) {
$this->beConstructedWith($authorizeClientApi, $genericApi, $payPalPaymentMethodProvider);
}

public function it_provides_data_from_provided_url(
PayPalPaymentMethodProviderInterface $payPalPaymentMethodProvider,
PaymentMethodInterface $paymentMethod,
CacheAuthorizeClientApiInterface $authorizeClientApi,
GenericApiInterface $genericApi
): void {
$payPalPaymentMethodProvider->provide()->willReturn($paymentMethod);
$authorizeClientApi->authorize($paymentMethod)->willReturn('TOKEN');
$genericApi->get('TOKEN', 'https://get-refund-data.com')->willReturn(
[
'links' => [
['rel' => 'self', 'href' => 'https://self.url.com'],
['rel' => 'up', 'href' => 'https://up.url.com'],
],
],
);

$genericApi->get('TOKEN', 'https://up.url.com')->shouldBeCalled();

$this->provide('https://get-refund-data.com');
}

public function it_throws_error_if_paypal_data_doesnt_contain_url(
PayPalPaymentMethodProviderInterface $payPalPaymentMethodProvider,
PaymentMethodInterface $paymentMethod,
CacheAuthorizeClientApiInterface $authorizeClientApi,
GenericApiInterface $genericApi
): void {
$payPalPaymentMethodProvider->provide()->willReturn($paymentMethod);
$authorizeClientApi->authorize($paymentMethod)->willReturn('TOKEN');
$genericApi->get('TOKEN', 'https://get-refund-data.com')->willReturn(
[
'links' => [
['rel' => 'self', 'href' => 'https://self.url.com'],
['rel' => 'get', 'href' => 'https://get.url.com'],
],
],
);

$this->shouldThrow(PayPalWrongDataException::class)->during('provide', ['https://get-refund-data.com']);
}
}
31 changes: 31 additions & 0 deletions src/Api/GenericApi.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Sylius\PayPalPlugin\Api;

use Symfony\Contracts\HttpClient\HttpClientInterface;

final class GenericApi implements GenericApiInterface
{
/** @var HttpClientInterface */
private $client;

public function __construct(HttpClientInterface $client)
{
$this->client = $client;
}

public function get(string $token, string $url): array
{
$options = [
'headers' => [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
];

return $this->client->request('GET', $url, $options)->toArray();
}
}
10 changes: 10 additions & 0 deletions src/Api/GenericApiInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Sylius\PayPalPlugin\Api;

interface GenericApiInterface
{
public function get(string $token, string $url): array;
}
30 changes: 24 additions & 6 deletions src/Controller/Webhook/RefundOrderAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
use Sylius\Component\Payment\PaymentTransitions;
use Sylius\Component\Resource\StateMachine\StateMachineInterface;
use Sylius\PayPalPlugin\Exception\PaymentNotFoundException;
use Sylius\PayPalPlugin\Exception\PayPalWrongDataException;
use Sylius\PayPalPlugin\Provider\PaymentProviderInterface;
use Sylius\PayPalPlugin\Provider\PayPalRefundDataProviderInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
Expand All @@ -26,40 +28,56 @@ final class RefundOrderAction
/** @var ObjectManager */
private $paymentManager;

/** @var PayPalRefundDataProviderInterface */
private $payPalRefundDataProvider;

public function __construct(
FactoryInterface $stateMachineFactory,
PaymentProviderInterface $paymentProvider,
ObjectManager $paymentManager
ObjectManager $paymentManager,
PayPalRefundDataProviderInterface $payPalRefundDataProvider
) {
$this->stateMachineFactory = $stateMachineFactory;
$this->paymentProvider = $paymentProvider;
$this->paymentManager = $paymentManager;
$this->payPalRefundDataProvider = $payPalRefundDataProvider;
}

public function __invoke(Request $request): Response
{
$refundData = $this->payPalRefundDataProvider->provide($this->getPayPalPaymentUrl($request));

try {
$payment = $this->paymentProvider->getByPayPalOrderId($this->getPayPalOrderId($request));
$payment = $this->paymentProvider->getByPayPalOrderId((string) $refundData['id']);
} catch (PaymentNotFoundException $exception) {
return new JsonResponse(['error' => $exception->getMessage()], Response::HTTP_NOT_FOUND);
}

/** @var StateMachineInterface $stateMachine */
$stateMachine = $this->stateMachineFactory->get($payment, PaymentTransitions::GRAPH);
$stateMachine->apply(PaymentTransitions::TRANSITION_REFUND);
if ($stateMachine->can(PaymentTransitions::TRANSITION_REFUND)) {
$stateMachine->apply(PaymentTransitions::TRANSITION_REFUND);
}

$this->paymentManager->flush();

return new JsonResponse([], Response::HTTP_NO_CONTENT);
}

private function getPayPalOrderId(Request $request): string
private function getPayPalPaymentUrl(Request $request): string
{
$content = (array) json_decode((string) $request->getContent(false), true);
Assert::keyExists($content, 'resource');
$resource = (array) $content['resource'];
Assert::keyExists($resource, 'id');
Assert::keyExists($resource, 'links');

/** @var string[] $link */
foreach ($resource['links'] as $link) {
if ($link['rel'] === 'up') {
return (string) $link['href'];
}
}

return (string) $resource['id'];
throw new PayPalWrongDataException();
}
}
13 changes: 13 additions & 0 deletions src/Exception/PayPalPaymentMethodNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Sylius\PayPalPlugin\Exception;

final class PayPalPaymentMethodNotFoundException extends \Exception
{
public function __construct()
{
parent::__construct('PayPal payment method not found');
}
}
13 changes: 13 additions & 0 deletions src/Exception/PayPalWrongDataException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Sylius\PayPalPlugin\Exception;

final class PayPalWrongDataException extends \Exception
{
public function __construct()
{
parent::__construct('PayPal data does not contain links to order');
}
}
3 changes: 0 additions & 3 deletions src/Processor/PayPalPaymentRefundProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
use Sylius\PayPalPlugin\Exception\PayPalOrderRefundException;
use Sylius\PayPalPlugin\Generator\PayPalAuthAssertionGeneratorInterface;
use Sylius\PayPalPlugin\Provider\RefundReferenceNumberProviderInterface;
use Webmozart\Assert\Assert;

final class PayPalPaymentRefundProcessor implements PaymentRefundProcessorInterface
{
Expand Down Expand Up @@ -91,8 +90,6 @@ public function refund(PaymentInterface $payment): void
(string) (((int) $payment->getAmount()) / 100),
(string) $order->getCurrencyCode()
);

Assert::same($response['status'], 'COMPLETED');
} catch (ClientException | \InvalidArgumentException $exception) {
throw new PayPalOrderRefundException();
}
Expand Down
38 changes: 38 additions & 0 deletions src/Provider/PayPalPaymentMethodProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Sylius\PayPalPlugin\Provider;

use Sylius\Bundle\PayumBundle\Model\GatewayConfigInterface;
use Sylius\Component\Core\Model\PaymentMethodInterface;
use Sylius\Component\Core\Repository\PaymentMethodRepositoryInterface;
use Sylius\PayPalPlugin\Exception\PayPalPaymentMethodNotFoundException;

final class PayPalPaymentMethodProvider implements PayPalPaymentMethodProviderInterface
{
/** @var PaymentMethodRepositoryInterface */
private $paymentMethodRepository;

public function __construct(PaymentMethodRepositoryInterface $paymentMethodRepository)
{
$this->paymentMethodRepository = $paymentMethodRepository;
}

public function provide(): PaymentMethodInterface
{
$payments = $this->paymentMethodRepository->findAll();

/** @var PaymentMethodInterface $payment */
foreach ($payments as $payment) {
/** @var GatewayConfigInterface $gatewayConfig */
$gatewayConfig = $payment->getGatewayConfig();

if ($gatewayConfig->getFactoryName() === 'sylius.pay_pal') {
return $payment;
}
}

throw new PayPalPaymentMethodNotFoundException();
}
}
14 changes: 14 additions & 0 deletions src/Provider/PayPalPaymentMethodProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Sylius\PayPalPlugin\Provider;

use Sylius\Component\Core\Model\PaymentMethodInterface;
use Sylius\PayPalPlugin\Exception\PayPalPaymentMethodNotFoundException;

interface PayPalPaymentMethodProviderInterface
{
/** @throws PayPalPaymentMethodNotFoundException */
public function provide(): PaymentMethodInterface;
}
48 changes: 48 additions & 0 deletions src/Provider/PayPalRefundDataProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Sylius\PayPalPlugin\Provider;

use Sylius\PayPalPlugin\Api\CacheAuthorizeClientApiInterface;
use Sylius\PayPalPlugin\Api\GenericApiInterface;
use Sylius\PayPalPlugin\Exception\PayPalWrongDataException;

final class PayPalRefundDataProvider implements PayPalRefundDataProviderInterface
{
/** @var CacheAuthorizeClientApiInterface */
private $authorizeClientApi;

/** @var PayPalPaymentMethodProviderInterface */
private $payPalPaymentMethodProvider;

/** @var GenericApiInterface */
private $genericApi;

public function __construct(
CacheAuthorizeClientApiInterface $authorizeClientApi,
GenericApiInterface $genericApi,
PayPalPaymentMethodProviderInterface $payPalPaymentMethodProvider
) {
$this->authorizeClientApi = $authorizeClientApi;
$this->genericApi = $genericApi;
$this->payPalPaymentMethodProvider = $payPalPaymentMethodProvider;
}

public function provide(string $url): array
{
$paymentMethod = $this->payPalPaymentMethodProvider->provide();
$token = $this->authorizeClientApi->authorize($paymentMethod);

$refundData = $this->genericApi->get($token, $url);

/** @var string[] $link */
foreach ($refundData['links'] as $link) {
if ($link['rel'] === 'up') {
return $this->genericApi->get($token, $link['href']);
}
}

throw new PayPalWrongDataException();
}
}
10 changes: 10 additions & 0 deletions src/Provider/PayPalRefundDataProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Sylius\PayPalPlugin\Provider;

interface PayPalRefundDataProviderInterface
{
public function provide(string $refundUrl): array;
}
Loading

0 comments on commit 7c77e21

Please sign in to comment.