src/Service/PayPalService.php line 70

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use Symfony\Contracts\HttpClient\HttpClientInterface;
  4. use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
  5. class PayPalService
  6. {
  7. private $httpClient;
  8. private $clientId;
  9. private $secret;
  10. private $mode;
  11. private $baseUrl;
  12. public function __construct(
  13. HttpClientInterface $httpClient,
  14. string $paypalClientId,
  15. string $paypalSecret,
  16. string $paypalMode
  17. ) {
  18. $this->httpClient = $httpClient;
  19. $this->clientId = $paypalClientId;
  20. $this->secret = $paypalSecret;
  21. $this->mode = strtolower(trim($paypalMode));
  22. // URL de base selon le mode
  23. $this->baseUrl = $this->mode === 'live'
  24. ? 'https://api-m.paypal.com'
  25. : 'https://api-m.sandbox.paypal.com';
  26. }
  27. /**
  28. * Obtenir un token d'accès PayPal
  29. */
  30. public function getAccessToken(): string
  31. {
  32. try {
  33. $response = $this->httpClient->request('POST', $this->baseUrl . '/v1/oauth2/token', [
  34. 'auth_basic' => [$this->clientId, $this->secret],
  35. 'headers' => [
  36. 'Content-Type' => 'application/x-www-form-urlencoded',
  37. ],
  38. 'body' => [
  39. 'grant_type' => 'client_credentials',
  40. ],
  41. ]);
  42. $statusCode = $response->getStatusCode();
  43. $content = $response->getContent(false);
  44. $data = json_decode($content, true);
  45. if ($statusCode >= 400) {
  46. throw new \RuntimeException('PayPal OAuth error (' . $statusCode . '): ' . $content);
  47. }
  48. if (!is_array($data) || !isset($data['access_token'])) {
  49. throw new \RuntimeException('PayPal OAuth response missing access_token: ' . $content);
  50. }
  51. return (string) $data['access_token'];
  52. } catch (TransportExceptionInterface $e) {
  53. throw new \RuntimeException('PayPal OAuth connection error: ' . $e->getMessage(), 0, $e);
  54. }
  55. }
  56. /**
  57. * Créer une commande PayPal
  58. */
  59. public function createOrder(
  60. float $amount,
  61. string $currency = 'EUR',
  62. string $orderId = null,
  63. ?string $returnUrl = null,
  64. ?string $cancelUrl = null
  65. ): array
  66. {
  67. $accessToken = $this->getAccessToken();
  68. try {
  69. $applicationContext = [
  70. 'brand_name' => 'Aromaforest',
  71. 'locale' => 'fr-FR',
  72. 'landing_page' => 'NO_PREFERENCE',
  73. 'shipping_preference' => 'NO_SHIPPING',
  74. 'user_action' => 'PAY_NOW',
  75. ];
  76. if ($returnUrl) {
  77. $applicationContext['return_url'] = $returnUrl;
  78. }
  79. if ($cancelUrl) {
  80. $applicationContext['cancel_url'] = $cancelUrl;
  81. }
  82. $response = $this->httpClient->request('POST', $this->baseUrl . '/v2/checkout/orders', [
  83. 'headers' => [
  84. 'Content-Type' => 'application/json',
  85. 'Authorization' => 'Bearer ' . $accessToken,
  86. ],
  87. 'json' => [
  88. 'intent' => 'CAPTURE',
  89. 'purchase_units' => [[
  90. 'reference_id' => $orderId ?? uniqid('ORDER_'),
  91. 'amount' => [
  92. 'currency_code' => $currency,
  93. 'value' => number_format($amount, 2, '.', ''),
  94. ],
  95. ]],
  96. 'application_context' => $applicationContext,
  97. ],
  98. ]);
  99. $statusCode = $response->getStatusCode();
  100. $content = $response->getContent(false);
  101. $data = json_decode($content, true);
  102. if ($statusCode >= 400) {
  103. throw new \RuntimeException('PayPal createOrder error (' . $statusCode . '): ' . $content);
  104. }
  105. if (!is_array($data) || !isset($data['id'])) {
  106. throw new \RuntimeException('PayPal createOrder response missing id: ' . $content);
  107. }
  108. return $data;
  109. } catch (TransportExceptionInterface $e) {
  110. throw new \RuntimeException('PayPal createOrder connection error: ' . $e->getMessage(), 0, $e);
  111. }
  112. }
  113. /**
  114. * Capturer le paiement d'une commande PayPal
  115. */
  116. public function captureOrder(string $orderId): array
  117. {
  118. $accessToken = $this->getAccessToken();
  119. try {
  120. $response = $this->httpClient->request(
  121. 'POST',
  122. $this->baseUrl . '/v2/checkout/orders/' . $orderId . '/capture',
  123. [
  124. 'headers' => [
  125. 'Content-Type' => 'application/json',
  126. 'Authorization' => 'Bearer ' . $accessToken,
  127. ],
  128. ]
  129. );
  130. $statusCode = $response->getStatusCode();
  131. $content = $response->getContent(false);
  132. $data = json_decode($content, true);
  133. if ($statusCode >= 400) {
  134. throw new \RuntimeException('PayPal captureOrder error (' . $statusCode . '): ' . $content);
  135. }
  136. if (!is_array($data)) {
  137. throw new \RuntimeException('PayPal captureOrder invalid response: ' . $content);
  138. }
  139. return $data;
  140. } catch (TransportExceptionInterface $e) {
  141. throw new \RuntimeException('PayPal captureOrder connection error: ' . $e->getMessage(), 0, $e);
  142. }
  143. }
  144. /**
  145. * Obtenir les détails d'une commande PayPal
  146. */
  147. public function getOrderDetails(string $orderId): array
  148. {
  149. $accessToken = $this->getAccessToken();
  150. try {
  151. $response = $this->httpClient->request(
  152. 'GET',
  153. $this->baseUrl . '/v2/checkout/orders/' . $orderId,
  154. [
  155. 'headers' => [
  156. 'Content-Type' => 'application/json',
  157. 'Authorization' => 'Bearer ' . $accessToken,
  158. ],
  159. ]
  160. );
  161. $statusCode = $response->getStatusCode();
  162. $content = $response->getContent(false);
  163. $data = json_decode($content, true);
  164. if ($statusCode >= 400) {
  165. throw new \RuntimeException('PayPal getOrderDetails error (' . $statusCode . '): ' . $content);
  166. }
  167. if (!is_array($data)) {
  168. throw new \RuntimeException('PayPal getOrderDetails invalid response: ' . $content);
  169. }
  170. return $data;
  171. } catch (TransportExceptionInterface $e) {
  172. throw new \RuntimeException('PayPal getOrderDetails connection error: ' . $e->getMessage(), 0, $e);
  173. }
  174. }
  175. /**
  176. * Obtenir le Client ID pour le frontend
  177. */
  178. public function getClientId(): string
  179. {
  180. return $this->clientId;
  181. }
  182. /**
  183. * Obtenir le mode (sandbox ou live)
  184. */
  185. public function getMode(): string
  186. {
  187. return $this->mode;
  188. }
  189. }