send('GET', $endpoint, [ 'query' => $query, 'headers' => $headers, ]); } public function post(string $endpoint, array|object $payload = [], array $headers = []): array { return $this->send('POST', $endpoint, [ 'json' => $payload, 'headers' => $headers, ]); } protected function send(string $method, string $endpoint, array $options = []): array { $request = $this->preparedRequest(); try { $response = $request->send(strtoupper($method), ltrim($endpoint, '/'), $options); } catch (RequestException $exception) { throw new PayPalException( $exception->getMessage(), $exception->response?->status(), $exception->response?->json() ?? [] ); } if ($response->failed()) { $body = $response->json() ?? []; $message = Arr::get($body, 'message') ?? Arr::get($body, 'details.0.description') ?? sprintf('PayPal request failed with status %s', $response->status()); throw new PayPalException($message, $response->status(), $body); } return $response->json() ?? []; } protected function preparedRequest(): PendingRequest { $baseUrl = $this->baseUrl(); $token = $this->accessToken(); return $this->http ->baseUrl($baseUrl) ->withHeaders([ 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'User-Agent' => sprintf('FotospielApp/%s PayPalClient', app()->version()), ]) ->withToken($token) ->acceptJson() ->asJson(); } protected function accessToken(): string { $cacheKey = 'paypal:access_token'; $cached = Cache::get($cacheKey); if (is_string($cached) && $cached !== '') { return $cached; } $tokenResponse = $this->requestAccessToken(); $accessToken = Arr::get($tokenResponse, 'access_token'); if (! is_string($accessToken) || $accessToken === '') { throw new PayPalException('PayPal access token missing from response.', null, $tokenResponse); } $expiresIn = (int) Arr::get($tokenResponse, 'expires_in', 0); if ($expiresIn > 60) { Cache::put($cacheKey, $accessToken, now()->addSeconds($expiresIn - 60)); } return $accessToken; } protected function requestAccessToken(): array { $clientId = config('services.paypal.client_id'); $secret = config('services.paypal.secret'); if (! $clientId || ! $secret) { throw new PayPalException('PayPal client credentials are not configured.'); } $response = $this->http ->baseUrl($this->baseUrl()) ->withBasicAuth($clientId, $secret) ->asForm() ->acceptJson() ->post('/v1/oauth2/token', [ 'grant_type' => 'client_credentials', ]); if ($response->failed()) { $body = $response->json() ?? []; $message = Arr::get($body, 'error_description') ?? Arr::get($body, 'message') ?? sprintf('PayPal OAuth failed with status %s', $response->status()); throw new PayPalException($message, $response->status(), $body); } return $response->json() ?? []; } protected function baseUrl(): string { $sandbox = (bool) config('services.paypal.sandbox', true); $baseUrl = $sandbox ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com'; $baseUrl = trim($baseUrl); if (! Str::startsWith($baseUrl, ['http://', 'https://'])) { $baseUrl = 'https://'.ltrim($baseUrl, '/'); } return rtrim($baseUrl, '/'); } }