Skip to content

Commit

Permalink
feature #99 Refunds improvements (Zales0123)
Browse files Browse the repository at this point in the history
This PR was merged into the 1.0-dev branch.

Discussion
----------

Refunds system improved to meet PayPal requirements.

Added header: `PayPal-Auth-Assertion` - generated with the special algorithm

Added request body: `{ "amount": { "value": "XX.XX", "currency_code": "XXX" }, "invoice_number": "XXXXXX" }`

Commits
-------

93febc5 Service for PayPal-Auth-Assertion header generation
4eff01c Pass PayPal-Auth-Assertion header during refund
1ff4a26 Get PayPal payment id from API during refund
7508454 Specify refunded amount
ddc7033 Generate reference number for refund
6141876 Fix tests
  • Loading branch information
SirDomin authored Sep 14, 2020
2 parents 88a8253 + 6141876 commit 69b779e
Show file tree
Hide file tree
Showing 21 changed files with 405 additions and 28 deletions.
1 change: 1 addition & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<file name="src/Controller/ProcessPayPalOrderAction.php"/>
<file name="src/DependencyInjection/SyliusPayPalExtension.php"/>
<file name="src/Payum/Action/CompleteOrderAction.php"/>
<file name="src/Processor/PayPalPaymentRefundProcessor.php"/>
</errorLevel>
</MixedArrayAccess>

Expand Down
12 changes: 10 additions & 2 deletions spec/Api/RefundPaymentApiSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,18 @@ function it_implements_refund_order_api_interface(): void
function it_refunds_pay_pal_payment_with_given_id(PayPalClientInterface $client): void
{
$client
->post('v2/payments/captures/123123/refund', 'TOKEN')
->post(
'v2/payments/captures/123123/refund',
'TOKEN',
['amount' => ['value' => '10.99', 'currency_code' => 'USD'], 'invoice_number' => '123-11-11-2010'],
['PayPal-Auth-Assertion' => 'PAY-PAL-AUTH-ASSERTION']
)
->willReturn(['status' => 'COMPLETED', 'id' => '123123'])
;

$this->refund('TOKEN', '123123')->shouldReturn(['status' => 'COMPLETED', 'id' => '123123']);
$this
->refund('TOKEN', '123123', 'PAY-PAL-AUTH-ASSERTION', '123-11-11-2010', '10.99', 'USD')
->shouldReturn(['status' => 'COMPLETED', 'id' => '123123'])
;
}
}
33 changes: 33 additions & 0 deletions spec/Client/PayPalClientSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,39 @@ function it_calls_post_request_on_paypal_api(
;
}

function it_calls_post_request_on_paypal_api_with_extra_headers(
ClientInterface $client,
ResponseInterface $response,
StreamInterface $body,
UuidProviderInterface $uuidProvider
): void {
$uuidProvider->provide()->willReturn('REQUEST-ID');

$client->request(
'POST',
'https://test-api.paypal.com/v2/post-request/',
[
'headers' => [
'Authorization' => 'Bearer TOKEN',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'PayPal-Partner-Attribution-Id' => 'TRACKING-ID',
'PayPal-Request-Id' => 'REQUEST-ID',
'CUSTOM_HEADER' => 'header',
],
'json' => ['parameter' => 'value', 'another_parameter' => 'another_value'],
]
)->willReturn($response);
$response->getStatusCode()->willReturn(200);
$response->getBody()->willReturn($body);
$body->getContents()->willReturn('{"status": "OK", "id": "123123"}');

$this
->post('v2/post-request/', 'TOKEN', ['parameter' => 'value', 'another_parameter' => 'another_value'], ['CUSTOM_HEADER' => 'header'])
->shouldReturn(['status' => 'OK', 'id' => '123123'])
;
}

function it_logs_debug_id_from_failed_post_request(
ClientInterface $client,
LoggerInterface $logger,
Expand Down
51 changes: 51 additions & 0 deletions spec/Generator/PayPalAuthAssertionGeneratorSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace spec\Sylius\PayPalPlugin\Generator;

use Payum\Core\Model\GatewayConfigInterface;
use PhpSpec\ObjectBehavior;
use Sylius\Component\Core\Model\PaymentMethodInterface;
use Sylius\PayPalPlugin\Generator\PayPalAuthAssertionGeneratorInterface;

final class PayPalAuthAssertionGeneratorSpec extends ObjectBehavior
{
function it_implements_pay_pal_auth_assertion_generator_interface(): void
{
$this->shouldImplement(PayPalAuthAssertionGeneratorInterface::class);
}

function it_generates_auth_assertion_based_on_payment_method_config(
PaymentMethodInterface $paymentMethod,
GatewayConfigInterface $gatewayConfig
): void {
$paymentMethod->getGatewayConfig()->willReturn($gatewayConfig);
$gatewayConfig->getConfig()->willReturn(['client_id' => 'CLIENT_ID', 'merchant_id' => 'MERCHANT_ID']);

$this
->generate($paymentMethod)
->shouldReturn('eyJhbGciOiJub25lIn0=.eyJpc3MiOiJDTElFTlRfSUQiLCJwYXllcl9pZCI6Ik1FUkNIQU5UX0lEIn0=.')
;
}

function it_throws_an_exception_if_gateway_config_does_not_have_proper_values_set(
PaymentMethodInterface $paymentMethod,
GatewayConfigInterface $gatewayConfig
): void {
$paymentMethod->getGatewayConfig()->willReturn($gatewayConfig, $gatewayConfig);
$gatewayConfig->getConfig()->willReturn(['merchant_id' => 'MERCHANT_ID'], ['client_id' => 'CLIENT_ID']);

$this->shouldThrow(\InvalidArgumentException::class)->during('generate', [$paymentMethod]);
$this->shouldThrow(\InvalidArgumentException::class)->during('generate', [$paymentMethod]);
}
}
98 changes: 85 additions & 13 deletions spec/Processor/PayPalPaymentRefundProcessorSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,33 @@
use Payum\Core\Model\GatewayConfigInterface;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\PaymentInterface;
use Sylius\Component\Core\Model\PaymentMethodInterface;
use Sylius\PayPalPlugin\Api\CacheAuthorizeClientApiInterface;
use Sylius\PayPalPlugin\Api\OrderDetailsApiInterface;
use Sylius\PayPalPlugin\Api\RefundPaymentApiInterface;
use Sylius\PayPalPlugin\Exception\PayPalOrderRefundException;
use Sylius\PayPalPlugin\Generator\PayPalAuthAssertionGeneratorInterface;
use Sylius\PayPalPlugin\Processor\PaymentRefundProcessorInterface;
use Sylius\PayPalPlugin\Provider\RefundReferenceNumberProviderInterface;

final class PayPalPaymentRefundProcessorSpec extends ObjectBehavior
{
function let(CacheAuthorizeClientApiInterface $authorizeClientApi, RefundPaymentApiInterface $refundOrderApi): void
{
$this->beConstructedWith($authorizeClientApi, $refundOrderApi);
function let(
CacheAuthorizeClientApiInterface $authorizeClientApi,
OrderDetailsApiInterface $orderDetailsApi,
RefundPaymentApiInterface $refundOrderApi,
PayPalAuthAssertionGeneratorInterface $payPalAuthAssertionGenerator,
RefundReferenceNumberProviderInterface $refundReferenceNumberProvider
): void {
$this->beConstructedWith(
$authorizeClientApi,
$orderDetailsApi,
$refundOrderApi,
$payPalAuthAssertionGenerator,
$refundReferenceNumberProvider
);
}

function it_implements_payment_refund_processor_interface(): void
Expand All @@ -38,18 +53,37 @@ function it_implements_payment_refund_processor_interface(): void

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

$authorizeClientApi->authorize($paymentMethod)->willReturn('TOKEN');
$refundOrderApi->refund('TOKEN', '123123')->willReturn(['status' => 'COMPLETED', 'id' => '123123']);
$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' => 'COMPLETED', 'id' => '123123'])
;

$this->refund($payment);
}
Expand All @@ -70,7 +104,7 @@ function it_does_nothing_if_payment_is_not_pay_pal(
$this->refund($payment);
}

function it_does_nothing_if_payment_is_payment_has_not_pay_pal_payment_id(
function it_does_nothing_if_payment_is_payment_has_not_pay_pal_order_id(
RefundPaymentApiInterface $refundOrderApi,
PaymentInterface $payment,
PaymentMethodInterface $paymentMethod,
Expand All @@ -89,18 +123,37 @@ function it_does_nothing_if_payment_is_payment_has_not_pay_pal_payment_id(

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
GatewayConfigInterface $gatewayConfig,
OrderInterface $order
): void {
$payment->getMethod()->willReturn($paymentMethod);
$paymentMethod->getGatewayConfig()->willReturn($gatewayConfig);
$gatewayConfig->getFactoryName()->willReturn('sylius.pay_pal');
$payment->getDetails()->willReturn(['paypal_payment_id' => '123123']);
$payment->getDetails()->willReturn(['paypal_order_id' => '123123']);

$authorizeClientApi->authorize($paymentMethod)->willReturn('TOKEN');
$refundOrderApi->refund('TOKEN', '123123')->willReturn(['status' => 'FAILED']);
$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)
Expand All @@ -110,18 +163,37 @@ function it_throws_exception_if_refund_could_not_be_processed(

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

$authorizeClientApi->authorize($paymentMethod)->willReturn('TOKEN');
$refundOrderApi->refund('TOKEN', '123123')->willThrow(ClientException::class);
$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')
->willThrow(ClientException::class)
;

$this
->shouldThrow(PayPalOrderRefundException::class)
Expand Down
33 changes: 33 additions & 0 deletions spec/Provider/RefundReferenceNumberProviderSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace spec\Sylius\PayPalPlugin\Provider;

use PhpSpec\ObjectBehavior;
use Sylius\Component\Core\Model\PaymentInterface;
use Sylius\PayPalPlugin\Provider\RefundReferenceNumberProviderInterface;

final class RefundReferenceNumberProviderSpec extends ObjectBehavior
{
function it_implements_refund_reference_number_provider_interface(): void
{
$this->shouldImplement(RefundReferenceNumberProviderInterface::class);
}

function it_provides_reference_number_based_on_payment_id_and_current_date(PaymentInterface $payment): void
{
$payment->getId()->willReturn(123);

$this->provide($payment)->shouldReturn('123-' . (new \DateTime())->format('d-m-Y'));
}
}
17 changes: 14 additions & 3 deletions src/Api/RefundPaymentApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,19 @@ public function __construct(PayPalClientInterface $client)
$this->client = $client;
}

public function refund(string $token, string $paymentId): array
{
return $this->client->post(sprintf('v2/payments/captures/%s/refund', $paymentId), $token);
public function refund(
string $token,
string $paymentId,
string $payPalAuthAssertion,
string $invoiceNumber,
string $amount,
string $currencyCode
): array {
return $this->client->post(
sprintf('v2/payments/captures/%s/refund', $paymentId),
$token,
['amount' => ['value' => $amount, 'currency_code' => $currencyCode], 'invoice_number' => $invoiceNumber],
['PayPal-Auth-Assertion' => $payPalAuthAssertion]
);
}
}
9 changes: 8 additions & 1 deletion src/Api/RefundPaymentApiInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,12 @@

interface RefundPaymentApiInterface
{
public function refund(string $token, string $paymentId): array;
public function refund(
string $token,
string $paymentId,
string $payPalAuthAssertion,
string $invoiceNumber,
string $amount,
string $currencyCode
): array;
}
6 changes: 4 additions & 2 deletions src/Client/PayPalClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ public function get(string $url, string $token): array
return $this->request('GET', $url, $token);
}

public function post(string $url, string $token, array $data = null): array
public function post(string $url, string $token, array $data = null, array $extraHeaders = []): array
{
return $this->request('POST', $url, $token, $data, ['PayPal-Request-Id' => $this->uuidProvider->provide()]);
$headers = array_merge($extraHeaders, ['PayPal-Request-Id' => $this->uuidProvider->provide()]);

return $this->request('POST', $url, $token, $data, $headers);
}

public function patch(string $url, string $token, array $data = null): array
Expand Down
2 changes: 1 addition & 1 deletion src/Client/PayPalClientInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public function authorize(string $clientId, string $clientSecret): array;

public function get(string $url, string $token): array;

public function post(string $url, string $token, array $data = null): array;
public function post(string $url, string $token, array $data = null, array $extraHeaders = []): array;

public function patch(string $url, string $token, array $data = null): array;
}
Loading

0 comments on commit 69b779e

Please sign in to comment.