From 086d918ee46dd0f213cf7852f549682fa90da49f Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Wed, 2 Oct 2024 14:34:26 +0200 Subject: [PATCH] Move paypal request logic to dedicated service --- config/services.yaml | 4 +- .../Economy/Payment/PaypalGatewayService.php | 144 ++++++++++++++++++ ...yTest.php => PaypalGatewayServiceTest.php} | 12 +- 3 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 src/Library/Economy/Payment/PaypalGatewayService.php rename tests/Library/Economy/Payment/{PaypalGatewayTest.php => PaypalGatewayServiceTest.php} (77%) diff --git a/config/services.yaml b/config/services.yaml index 87350c7..1e06935 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -58,9 +58,9 @@ services: - '%env(STRIPE_API_KEY)%' - '%env(STRIPE_WEBHOOK_SECRET)%' - App\Library\Economy\Payment\PaypalGateway: + App\Library\Economy\Payment\PaypalGatewayService: arguments: - - '%env(APP_ENV)%' + - '%env(PAYPAL_API_ADDRESS)%' - '%env(PAYPAL_CLIENT_ID)%' - '%env(PAYPAL_CLIENT_SECRET)%' - '%env(PAYPAL_WEBHOOK_ID)%' diff --git a/src/Library/Economy/Payment/PaypalGatewayService.php b/src/Library/Economy/Payment/PaypalGatewayService.php new file mode 100644 index 0000000..f077531 --- /dev/null +++ b/src/Library/Economy/Payment/PaypalGatewayService.php @@ -0,0 +1,144 @@ +cache = new FilesystemAdapter(); + + $httpOptions = new HttpOptions(); + $httpOptions->setBaseUri($paypalApiAddress); + + $this->setHttpClient($httpClient->withOptions($httpOptions->toArray())); + } + + public function setHttpClient(HttpClientInterface $httpClient): static + { + $this->httpClient = $httpClient; + + return $this; + } + + /** + * Calls the PayPal API to generate an OAuth2 token. + * + * @return array The API response body + */ + public function generateAuthToken(): array + { + $response = $this->httpClient->request(Request::METHOD_POST, '/v1/oauth2/token', [ + 'auth_basic' => [$this->paypalClientId, $this->paypalClientSecret], + 'body' => 'grant_type=client_credentials', + ]); + + return $response->toArray(); + } + + /** + * Retrieves the PayPal OAuth2 token data from cache (if available) or generates a new one. + * + * @return array The OAuth2 token data + */ + public function getAuthToken(): array + { + return $this->cache->get($this->paypalApiAddress, function (ItemInterface $item): array { + $tokenData = $this->generateAuthToken(); + + $item->expiresAfter($tokenData['expires_in']); + + return $tokenData; + }); + } + + public function postOrder(array $order): array + { + $response = $this->httpClient->request(Request::METHOD_POST, '/v2/checkout/orders', [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + 'json' => $order, + ]); + + $content = \json_decode($response->getContent(), true); + if (!\in_array($response->getStatusCode(), [Response::HTTP_OK, Response::HTTP_CREATED])) { + throw new \Exception($content['message']); + } + + return $content; + } + + public function getOrder(string $orderId): array + { + $request = $this->httpClient->request( + Request::METHOD_GET, + sprintf('/v2/checkout/orders/%s', $orderId), + [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + ] + ); + + if ($request->getStatusCode() !== Response::HTTP_OK) { + throw new \Exception(sprintf("PayPal checkout '%s' could not be requested.", $orderId)); + } + + return \json_decode($request->getContent(), true); + } + + public function captureOrder(array $order): array + { + $link = \array_filter($order['links'], function ($order) { + return $order['rel'] === 'capture'; + }); + + if (empty($link)) { + throw new \Exception(sprintf("PayPal checkout '%s' was not ready for capture.", $order['id'])); + } + + $link = \array_pop($link); + $request = $this->httpClient->request( + $link['method'], + $link['href'], + [ + 'auth_bearer' => $this->getAuthToken()['access_token'], + 'headers' => ['Content-Type' => 'application/json'], + ] + ); + + if ($request->getStatusCode() !== Response::HTTP_CREATED) { + throw new \Exception(sprintf("Payment capture for PayPal checkout '%s' was unsuccessful.", $order['id'])); + } + + return \json_decode($request->getContent(), true); + } + + public function verifyWebhook(Request $request): bool + { + $headers = $request->headers; + + return \openssl_verify( + implode('|', [ + $headers->get('paypal-transmission-id'), + $headers->get('paypal-transmission-type'), + $this->paypalWebhookId, + \crc32($request->getContent()), + ]), + \base64_decode($headers->get('paypal-transmission-sig')), + \openssl_pkey_get_public(\file_get_contents($headers->get('paypal-cert-url'))) + ) === 1; + } +} diff --git a/tests/Library/Economy/Payment/PaypalGatewayTest.php b/tests/Library/Economy/Payment/PaypalGatewayServiceTest.php similarity index 77% rename from tests/Library/Economy/Payment/PaypalGatewayTest.php rename to tests/Library/Economy/Payment/PaypalGatewayServiceTest.php index 441d36c..618282b 100644 --- a/tests/Library/Economy/Payment/PaypalGatewayTest.php +++ b/tests/Library/Economy/Payment/PaypalGatewayServiceTest.php @@ -2,20 +2,20 @@ namespace App\Tests\Library\Economy\Payment; -use App\Library\Economy\Payment\PaypalGateway; +use App\Library\Economy\Payment\PaypalGatewayService; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; -class PaypalGatewayTest extends KernelTestCase +class PaypalGatewayServiceTest extends KernelTestCase { - private PaypalGateway $paypal; + private PaypalGatewayService $paypalService; public function setUp(): void { self::bootKernel(); - $this->paypal = static::getContainer()->get(PaypalGateway::class); + $this->paypalService = static::getContainer()->get(PaypalGatewayService::class); } public function testAuthenticates(): void @@ -27,9 +27,9 @@ class PaypalGatewayTest extends KernelTestCase ]); $httpClient = new MockHttpClient($mockResponse, 'https://testapi.paypal.com'); - $this->paypal = $this->paypal->setHttpClient($httpClient); + $this->paypalService = $this->paypalService->setHttpClient($httpClient); - $responseData = $this->paypal->generateAuthToken(); + $responseData = $this->paypalService->generateAuthToken(); $this->assertSame('POST', $mockResponse->getRequestMethod()); $this->assertSame('https://testapi.paypal.com/v1/oauth2/token', $mockResponse->getRequestUrl());