diff --git a/CHANGELOG.md b/CHANGELOG.md index cc56d6a0b..894ebd63d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 16.0.0 - 2024-10-01 +* [#1756](https://github.com/stripe/stripe-php/pull/1756) Support for APIs in the new API version 2024-09-30.acacia + + This release changes the pinned API version to `2024-09-30.acacia`. Please read the [API Upgrade Guide](https://stripe.com/docs/upgrades#2024-09-30.acacia) and carefully review the API changes before upgrading. + + ### ⚠️ Breaking changes + + * Rename `usage_threshold_config` to `usage_threshold` on `Billing.Alert` + * Remove support for `filter` on `Billing.Alert`. Use the filters on the `usage_threshold` instead + + + ### Additions + + * Add support for new value `international_transaction` on enum `Treasury.ReceivedCredit.failure_code` + * Add support for new Usage Billing APIs `Billing.MeterEvent`, `Billing.MeterEventAdjustments`, `Billing.MeterEventSession`, `Billing.MeterEventStream` and the new Events API `Core.Events` under the [v2 namespace ](https://docs.corp.stripe.com/api-v2-overview) + * Add new method `parseThinEvent()` on the `StripeClient` class to parse [thin events](https://docs.corp.stripe.com/event-destinations#events-overview). + * Add a new method [rawRequest()](https://github.com/stripe/stripe-node/tree/master?tab=readme-ov-file#custom-requests) on the `StripeClient` class that takes a HTTP method type, url and relevant parameters to make requests to the Stripe API that are not yet supported in the SDK. + ## 15.11.0-beta.1 - 2024-09-18 * [#1748](https://github.com/stripe/stripe-php/pull/1748) Update generated code for beta * Remove support for resource `QuotePhase` @@ -39,8 +57,8 @@ ## 15.8.0 - 2024-08-29 * [#1742](https://github.com/stripe/stripe-php/pull/1742) Generate SDK for OpenAPI spec version 1230 - * Add support for new value `issuing_regulatory_reporting` on enum `File.purpose` - * Add support for new value `hr_oib` on enum `TaxId.type` + * Add support for new value `issuing_regulatory_reporting` on enum `File.purpose` + * Add support for new value `hr_oib` on enum `TaxId.type` * Add support for `status_details` on `TestHelpers.TestClock` ## 15.8.0-beta.1 - 2024-08-15 diff --git a/OPENAPI_VERSION b/OPENAPI_VERSION index 5f5b31119..8f166ae2e 100644 --- a/OPENAPI_VERSION +++ b/OPENAPI_VERSION @@ -1 +1 @@ -v1267 \ No newline at end of file +v1268 \ No newline at end of file diff --git a/examples/MeterEventStream.php b/examples/MeterEventStream.php new file mode 100644 index 000000000..cce30a0d6 --- /dev/null +++ b/examples/MeterEventStream.php @@ -0,0 +1,53 @@ +apiKey = $apiKey; + $this->meterEventSession = null; + } + + private function refreshMeterEventSession() + { + // Check if session is null or expired + if ( + null === $this->meterEventSession + || $this->meterEventSession->expires_at <= time() + ) { + // Create a new meter event session in case the existing session expired + $client = new \Stripe\StripeClient($this->apiKey); + $this->meterEventSession = $client->v2->billing->meterEventSession->create(); + } + } + + public function sendMeterEvent($meterEvent) + { + // Refresh the meter event session, if necessary + $this->refreshMeterEventSession(); + + // Create a meter event with the current session's authentication token + $client = new \Stripe\StripeClient($this->meterEventSession->authentication_token); + $client->v2->billing->meterEventStream->create([ + 'events' => [$meterEvent], + ]); + } +} + +// Usage +$apiKey = '{{API_KEY}}'; +$customerId = '{{CUSTOMER_ID}}'; + +$manager = new MeterEventStream($apiKey); +$manager->sendMeterEvent([ + 'event_name' => 'alpaca_ai_tokens', + 'payload' => [ + 'stripe_customer_id' => $customerId, + 'value' => '26', + ], +]); diff --git a/examples/NewExample.php b/examples/NewExample.php new file mode 100644 index 000000000..bde7007dc --- /dev/null +++ b/examples/NewExample.php @@ -0,0 +1,26 @@ +apiKey = $apiKey; + } + + public function doSomethingGreat() + { + echo "Hello World\n"; + // $client = new \Stripe\StripeClient($this->apiKey); + } +} + +// Usage +$apiKey = '{{API_KEY}}'; + +$example = new NewExample($apiKey); +$example->doSomethingGreat(); diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..8ba8046cb --- /dev/null +++ b/examples/README.md @@ -0,0 +1,11 @@ +## Running an example + +From the examples folder, run: +`php your_example.php` + +## Adding a new example + +1. Clone new_example.php +2. Implement your example +3. Run it (as per above) +4. 👍 diff --git a/examples/stripe_webhook_handler.php b/examples/stripe_webhook_handler.php new file mode 100644 index 000000000..b81ae2414 --- /dev/null +++ b/examples/stripe_webhook_handler.php @@ -0,0 +1,34 @@ +post('/webhook', function ($request, $response) use ($client, $webhook_secret) { + $webhook_body = $request->getBody()->getContents(); + $sig_header = $request->getHeaderLine('Stripe-Signature'); + + try { + $thin_event = $client->parseThinEvent($webhook_body, $sig_header, $webhook_secret); + + // Fetch the event data to understand the failure + $event = $client->v2->core->events->retrieve($thin_event->id); + if ($event instanceof \Stripe\Events\V1BillingMeterErrorReportTriggeredEvent) { + $meter = $event->fetchRelatedObject(); + $meter_id = $meter->id; + + // Record the failures and alert your team + // Add your logic here + } + + return $response->withStatus(200); + } catch (\Exception $e) { + return $response->withStatus(400)->withJson(['error' => $e->getMessage()]); + } +}); + +$app->run(); diff --git a/init.php b/init.php index 3f6b02e06..7153dfb49 100644 --- a/init.php +++ b/init.php @@ -13,6 +13,7 @@ require __DIR__ . '/lib/Util/RequestOptions.php'; require __DIR__ . '/lib/Util/Set.php'; require __DIR__ . '/lib/Util/Util.php'; +require __DIR__ . '/lib/Util/EventTypes.php'; require __DIR__ . '/lib/Util/ObjectTypes.php'; // HttpClient @@ -65,11 +66,15 @@ require __DIR__ . '/lib/ApiRequestor.php'; require __DIR__ . '/lib/ApiResource.php'; require __DIR__ . '/lib/SingletonApiResource.php'; +require __DIR__ . '/lib/Service/ServiceNavigatorTrait.php'; require __DIR__ . '/lib/Service/AbstractService.php'; require __DIR__ . '/lib/Service/AbstractServiceFactory.php'; -require __DIR__ . '/lib/Preview.php'; - +require __DIR__ . '/lib/V2/Event.php'; +require __DIR__ . '/lib/ThinEvent.php'; +require __DIR__ . '/lib/Reason.php'; +require __DIR__ . '/lib/RelatedObject.php'; require __DIR__ . '/lib/Collection.php'; +require __DIR__ . '/lib/V2/Collection.php'; require __DIR__ . '/lib/SearchResult.php'; require __DIR__ . '/lib/ErrorObject.php'; require __DIR__ . '/lib/Issuing/CardDetails.php'; @@ -96,6 +101,9 @@ require __DIR__ . '/lib/BankAccount.php'; require __DIR__ . '/lib/Billing/Alert.php'; require __DIR__ . '/lib/Billing/AlertTriggered.php'; +require __DIR__ . '/lib/Billing/CreditBalanceSummary.php'; +require __DIR__ . '/lib/Billing/CreditBalanceTransaction.php'; +require __DIR__ . '/lib/Billing/CreditGrant.php'; require __DIR__ . '/lib/Billing/Meter.php'; require __DIR__ . '/lib/Billing/MeterErrorReport.php'; require __DIR__ . '/lib/Billing/MeterEvent.php'; @@ -131,6 +139,11 @@ require __DIR__ . '/lib/Entitlements/Feature.php'; require __DIR__ . '/lib/EphemeralKey.php'; require __DIR__ . '/lib/Event.php'; +require __DIR__ . '/lib/EventData/V1BillingMeterErrorReportTriggeredEventData.php'; +require __DIR__ . '/lib/EventData/V1BillingMeterNoMeterFoundEventData.php'; +require __DIR__ . '/lib/Events/V1BillingMeterErrorReportTriggeredEvent.php'; +require __DIR__ . '/lib/Events/V1BillingMeterNoMeterFoundEvent.php'; +require __DIR__ . '/lib/Exception/TemporarySessionExpiredException.php'; require __DIR__ . '/lib/ExchangeRate.php'; require __DIR__ . '/lib/File.php'; require __DIR__ . '/lib/FileLink.php'; @@ -204,6 +217,9 @@ require __DIR__ . '/lib/Service/BalanceTransactionService.php'; require __DIR__ . '/lib/Service/Billing/AlertService.php'; require __DIR__ . '/lib/Service/Billing/BillingServiceFactory.php'; +require __DIR__ . '/lib/Service/Billing/CreditBalanceSummaryService.php'; +require __DIR__ . '/lib/Service/Billing/CreditBalanceTransactionService.php'; +require __DIR__ . '/lib/Service/Billing/CreditGrantService.php'; require __DIR__ . '/lib/Service/Billing/MeterEventAdjustmentService.php'; require __DIR__ . '/lib/Service/Billing/MeterEventService.php'; require __DIR__ . '/lib/Service/Billing/MeterService.php'; @@ -344,6 +360,14 @@ require __DIR__ . '/lib/Service/Treasury/TransactionEntryService.php'; require __DIR__ . '/lib/Service/Treasury/TransactionService.php'; require __DIR__ . '/lib/Service/Treasury/TreasuryServiceFactory.php'; +require __DIR__ . '/lib/Service/V2/Billing/BillingServiceFactory.php'; +require __DIR__ . '/lib/Service/V2/Billing/MeterEventAdjustmentService.php'; +require __DIR__ . '/lib/Service/V2/Billing/MeterEventService.php'; +require __DIR__ . '/lib/Service/V2/Billing/MeterEventSessionService.php'; +require __DIR__ . '/lib/Service/V2/Billing/MeterEventStreamService.php'; +require __DIR__ . '/lib/Service/V2/Core/CoreServiceFactory.php'; +require __DIR__ . '/lib/Service/V2/Core/EventService.php'; +require __DIR__ . '/lib/Service/V2/V2ServiceFactory.php'; require __DIR__ . '/lib/Service/WebhookEndpointService.php'; require __DIR__ . '/lib/SetupAttempt.php'; require __DIR__ . '/lib/SetupIntent.php'; @@ -390,6 +414,9 @@ require __DIR__ . '/lib/Treasury/TransactionEntry.php'; require __DIR__ . '/lib/UsageRecord.php'; require __DIR__ . '/lib/UsageRecordSummary.php'; +require __DIR__ . '/lib/V2/Billing/MeterEvent.php'; +require __DIR__ . '/lib/V2/Billing/MeterEventAdjustment.php'; +require __DIR__ . '/lib/V2/Billing/MeterEventSession.php'; require __DIR__ . '/lib/WebhookEndpoint.php'; // The end of the section generated from our OpenAPI spec diff --git a/lib/ApiOperations/Request.php b/lib/ApiOperations/Request.php index 86842acbb..3f33e7df9 100644 --- a/lib/ApiOperations/Request.php +++ b/lib/ApiOperations/Request.php @@ -32,15 +32,16 @@ protected static function _validateParams($params = null) * @param array $params list of parameters for the request * @param null|array|string $options * @param string[] $usage names of tracked behaviors associated with this request + * @param 'v1'|'v2' $apiMode * * @throws \Stripe\Exception\ApiErrorException if the request fails * * @return array tuple containing (the JSON response, $options) */ - protected function _request($method, $url, $params = [], $options = null, $usage = []) + protected function _request($method, $url, $params = [], $options = null, $usage = [], $apiMode = 'v1') { $opts = $this->_opts->merge($options); - list($resp, $options) = static::_staticRequest($method, $url, $params, $opts, $usage); + list($resp, $options) = static::_staticRequest($method, $url, $params, $opts, $usage, $apiMode); $this->setLastResponse($resp); return [$resp->json, $options]; @@ -96,17 +97,18 @@ protected function _requestStream($method, $url, $readBodyChunk, $params = [], $ * @param array $params list of parameters for the request * @param null|array|string $options * @param string[] $usage names of tracked behaviors associated with this request + * @param 'v1'|'v2' $apiMode * * @throws \Stripe\Exception\ApiErrorException if the request fails * * @return array tuple containing (the JSON response, $options) */ - protected static function _staticRequest($method, $url, $params, $options, $usage = []) + protected static function _staticRequest($method, $url, $params, $options, $usage = [], $apiMode = 'v1') { $opts = \Stripe\Util\RequestOptions::parse($options); $baseUrl = isset($opts->apiBase) ? $opts->apiBase : static::baseUrl(); $requestor = new \Stripe\ApiRequestor($opts->apiKey, $baseUrl); - list($response, $opts->apiKey) = $requestor->request($method, $url, $params, $opts->headers, 'standard', $usage); + list($response, $opts->apiKey) = $requestor->request($method, $url, $params, $opts->headers, $apiMode, $usage); $opts->discardNonPersistentHeaders(); return [$response, $opts]; diff --git a/lib/ApiRequestor.php b/lib/ApiRequestor.php index a790ef9a7..f2bbde6ef 100644 --- a/lib/ApiRequestor.php +++ b/lib/ApiRequestor.php @@ -36,7 +36,7 @@ class ApiRequestor */ private static $requestTelemetry; - private static $OPTIONS_KEYS = ['api_key', 'idempotency_key', 'stripe_account', 'stripe_version', 'api_base']; + private static $OPTIONS_KEYS = ['api_key', 'idempotency_key', 'stripe_account', 'stripe_context', 'stripe_version', 'api_base']; /** * ApiRequestor constructor. @@ -120,20 +120,20 @@ private static function _encodeObjects($d) * @param string $url * @param null|array $params * @param null|array $headers - * @param 'preview'|'standard' $apiMode + * @param 'v1'|'v2' $apiMode * @param string[] $usage * * @throws Exception\ApiErrorException * * @return array tuple containing (ApiReponse, API key) */ - public function request($method, $url, $params = null, $headers = null, $apiMode = 'standard', $usage = []) + public function request($method, $url, $params = null, $headers = null, $apiMode = 'v1', $usage = []) { $params = $params ?: []; $headers = $headers ?: []; list($rbody, $rcode, $rheaders, $myApiKey) = $this->_requestRaw($method, $url, $params, $headers, $apiMode, $usage); - $json = $this->_interpretResponse($rbody, $rcode, $rheaders); + $json = $this->_interpretResponse($rbody, $rcode, $rheaders, $apiMode); $resp = new ApiResponse($rbody, $rcode, $rheaders, $json); return [$resp, $myApiKey]; @@ -145,19 +145,19 @@ public function request($method, $url, $params = null, $headers = null, $apiMode * @param callable $readBodyChunkCallable * @param null|array $params * @param null|array $headers - * @param 'preview'|'standard' $apiMode + * @param 'v1'|'v2' $apiMode * @param string[] $usage * * @throws Exception\ApiErrorException */ - public function requestStream($method, $url, $readBodyChunkCallable, $params = null, $headers = null, $apiMode = 'standard', $usage = []) + public function requestStream($method, $url, $readBodyChunkCallable, $params = null, $headers = null, $apiMode = 'v1', $usage = []) { $params = $params ?: []; $headers = $headers ?: []; list($rbody, $rcode, $rheaders, $myApiKey) = $this->_requestRawStreaming($method, $url, $params, $headers, $apiMode, $usage, $readBodyChunkCallable); if ($rcode >= 300) { - $this->_interpretResponse($rbody, $rcode, $rheaders); + $this->_interpretResponse($rbody, $rcode, $rheaders, $apiMode); } } @@ -166,11 +166,12 @@ public function requestStream($method, $url, $readBodyChunkCallable, $params = n * @param int $rcode * @param array $rheaders * @param array $resp + * @param 'v1'|'v2' $apiMode * * @throws Exception\UnexpectedValueException * @throws Exception\ApiErrorException */ - public function handleErrorResponse($rbody, $rcode, $rheaders, $resp) + public function handleErrorResponse($rbody, $rcode, $rheaders, $resp, $apiMode) { if (!\is_array($resp) || !isset($resp['error'])) { $msg = "Invalid response object from API: {$rbody} " @@ -182,11 +183,12 @@ public function handleErrorResponse($rbody, $rcode, $rheaders, $resp) $errorData = $resp['error']; $error = null; + if (\is_string($errorData)) { $error = self::_specificOAuthError($rbody, $rcode, $rheaders, $resp, $errorData); } if (!$error) { - $error = self::_specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData); + $error = 'v1' === $apiMode ? self::_specificV1APIError($rbody, $rcode, $rheaders, $resp, $errorData) : self::_specificV2APIError($rbody, $rcode, $rheaders, $resp, $errorData); } throw $error; @@ -203,7 +205,7 @@ public function handleErrorResponse($rbody, $rcode, $rheaders, $resp) * * @return Exception\ApiErrorException */ - private static function _specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData) + private static function _specificV1APIError($rbody, $rcode, $rheaders, $resp, $errorData) { $msg = isset($errorData['message']) ? $errorData['message'] : (isset($errorData['developer_message']) ? $errorData['developer_message'] : null); $param = isset($errorData['param']) ? $errorData['param'] : null; @@ -222,6 +224,7 @@ private static function _specificAPIError($rbody, $rcode, $rheaders, $resp, $err return Exception\IdempotencyException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code); } + // fall through in generic 400 or 404, returns InvalidRequestException by default // no break case 404: return Exception\InvalidRequestException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $param); @@ -243,6 +246,43 @@ private static function _specificAPIError($rbody, $rcode, $rheaders, $resp, $err } } + /** + * @static + * + * @param string $rbody + * @param int $rcode + * @param array $rheaders + * @param array $resp + * @param array $errorData + * + * @return Exception\ApiErrorException + */ + private static function _specificV2APIError($rbody, $rcode, $rheaders, $resp, $errorData) + { + $msg = isset($errorData['message']) ? $errorData['message'] : null; + $code = isset($errorData['code']) ? $errorData['code'] : null; + $type = isset($errorData['type']) ? $errorData['type'] : null; + + switch ($type) { + case 'idempotency_error': + return Exception\IdempotencyException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code); + // The beginning of the section generated from our OpenAPI spec + case 'temporary_session_expired': + return Exception\TemporarySessionExpiredException::factory( + $msg, + $rcode, + $rbody, + $resp, + $rheaders, + $code + ); + + // The end of the section generated from our OpenAPI spec + default: + return self::_specificV1APIError($rbody, $rcode, $rheaders, $resp, $errorData); + } + } + /** * @static * @@ -332,12 +372,13 @@ private static function _isDisabled($disableFunctionsOutput, $functionName) * @param string $apiKey the Stripe API key, to be used in regular API requests * @param null $clientInfo client user agent information * @param null $appInfo information to identify a plugin that integrates Stripe using this library + * @param 'v1'|'v2' $apiMode * * @return array */ - private static function _defaultHeaders($apiKey, $clientInfo = null, $appInfo = null) + private static function _defaultHeaders($apiKey, $clientInfo = null, $appInfo = null, $apiMode = 'v1') { - $uaString = 'Stripe/v1 PhpBindings/' . Stripe::VERSION; + $uaString = "Stripe/{$apiMode} PhpBindings/" . Stripe::VERSION; $langVersion = \PHP_VERSION; $uname_disabled = self::_isDisabled(\ini_get('disable_functions'), 'php_uname'); @@ -373,7 +414,7 @@ private static function _defaultHeaders($apiKey, $clientInfo = null, $appInfo = * @param string $url * @param array $params * @param array $headers - * @param 'preview'|'standard' $apiMode + * @param 'v1'|'v2' $apiMode */ private function _prepareRequest($method, $url, $params, $headers, $apiMode) { @@ -415,10 +456,10 @@ function ($key) use ($params) { } $absUrl = $this->_apiBase . $url; - if ('standard' === $apiMode) { + if ('v1' === $apiMode) { $params = self::_encodeObjects($params); } - $defaultHeaders = $this->_defaultHeaders($myApiKey, $clientUAInfo, $this->_appInfo); + $defaultHeaders = $this->_defaultHeaders($myApiKey, $clientUAInfo, $this->_appInfo, $apiMode); if (Stripe::$accountId) { $defaultHeaders['Stripe-Account'] = Stripe::$accountId; @@ -440,9 +481,9 @@ function ($key) use ($params) { if ($hasFile) { $defaultHeaders['Content-Type'] = 'multipart/form-data'; - } elseif ('preview' === $apiMode) { + } elseif ('v2' === $apiMode) { $defaultHeaders['Content-Type'] = 'application/json'; - } elseif ('standard' === $apiMode) { + } elseif ('v1' === $apiMode) { $defaultHeaders['Content-Type'] = 'application/x-www-form-urlencoded'; } else { throw new Exception\InvalidArgumentException('Unknown API mode: ' . $apiMode); @@ -463,7 +504,7 @@ function ($key) use ($params) { * @param string $url * @param array $params * @param array $headers - * @param 'preview'|'standard' $apiMode + * @param 'v1'|'v2' $apiMode * @param string[] $usage * * @throws Exception\AuthenticationException @@ -508,7 +549,7 @@ private function _requestRaw($method, $url, $params, $headers, $apiMode, $usage) * @param array $headers * @param string[] $usage * @param callable $readBodyChunkCallable - * @param 'preview'|'standard' $apiMode + * @param 'v1'|'v2' $apiMode * * @throws Exception\AuthenticationException * @throws Exception\ApiConnectionException @@ -574,13 +615,14 @@ private function _processResourceParam($resource) * @param string $rbody * @param int $rcode * @param array $rheaders + * @param 'v1'|'v2' $apiMode * * @throws Exception\UnexpectedValueException * @throws Exception\ApiErrorException * * @return array */ - private function _interpretResponse($rbody, $rcode, $rheaders) + private function _interpretResponse($rbody, $rcode, $rheaders, $apiMode) { $resp = \json_decode($rbody, true); $jsonError = \json_last_error(); @@ -592,7 +634,7 @@ private function _interpretResponse($rbody, $rcode, $rheaders) } if ($rcode < 200 || $rcode >= 300) { - $this->handleErrorResponse($rbody, $rcode, $rheaders, $resp); + $this->handleErrorResponse($rbody, $rcode, $rheaders, $resp, $apiMode); } return $resp; diff --git a/lib/BaseStripeClient.php b/lib/BaseStripeClient.php index 11501c3ff..a2eb04a7b 100644 --- a/lib/BaseStripeClient.php +++ b/lib/BaseStripeClient.php @@ -2,6 +2,8 @@ namespace Stripe; +use Stripe\Util\Util; + class BaseStripeClient implements StripeClientInterface, StripeStreamingClientInterface { /** @var string default base URL for Stripe's API */ @@ -13,16 +15,21 @@ class BaseStripeClient implements StripeClientInterface, StripeStreamingClientIn /** @var string default base URL for Stripe's Files API */ const DEFAULT_FILES_BASE = 'https://files.stripe.com'; + /** @var string default base URL for Stripe's Meter Events API */ + const DEFAULT_METER_EVENTS_BASE = 'https://meter-events.stripe.com'; + /** @var array */ const DEFAULT_CONFIG = [ 'api_key' => null, 'app_info' => null, 'client_id' => null, 'stripe_account' => null, + 'stripe_context' => null, 'stripe_version' => \Stripe\Util\ApiVersion::CURRENT, 'api_base' => self::DEFAULT_API_BASE, 'connect_base' => self::DEFAULT_CONNECT_BASE, 'files_base' => self::DEFAULT_FILES_BASE, + 'meter_events_base' => self::DEFAULT_METER_EVENTS_BASE, ]; /** @var array */ @@ -31,9 +38,6 @@ class BaseStripeClient implements StripeClientInterface, StripeStreamingClientIn /** @var \Stripe\Util\RequestOptions */ private $defaultOpts; - /** @var \Stripe\Preview */ - public $preview; - /** * Initializes a new instance of the {@link BaseStripeClient} class. * @@ -48,6 +52,8 @@ class BaseStripeClient implements StripeClientInterface, StripeStreamingClientIn * - client_id (null|string): the Stripe client ID, to be used in OAuth requests. * - stripe_account (null|string): a Stripe account ID. If set, all requests sent by the client * will automatically use the {@code Stripe-Account} header with that account ID. + * - stripe_context (null|string): a Stripe account or compartment ID. If set, all requests sent by the client + * will automatically use the {@code Stripe-Context} header with that ID. * - stripe_version (null|string): a Stripe API version. If set, all requests sent by the client * will include the {@code Stripe-Version} header with that API version. * @@ -60,6 +66,8 @@ class BaseStripeClient implements StripeClientInterface, StripeStreamingClientIn * {@link DEFAULT_CONNECT_BASE}. * - files_base (string): the base URL for file creation requests. Defaults to * {@link DEFAULT_FILES_BASE}. + * - meter_events_base (string): the base URL for high throughput requests. Defaults to + * {@link DEFAULT_METER_EVENTS_BASE}. * * @param array|string $config the API key as a string, or an array containing * the client configuration settings @@ -79,10 +87,9 @@ public function __construct($config = []) $this->defaultOpts = \Stripe\Util\RequestOptions::parse([ 'stripe_account' => $config['stripe_account'], + 'stripe_context' => $config['stripe_context'], 'stripe_version' => $config['stripe_version'], ]); - - $this->preview = new Preview($this); } /** @@ -135,6 +142,16 @@ public function getFilesBase() return $this->config['files_base']; } + /** + * Gets the base URL for Stripe's Meter Events API. + * + * @return string the base URL for Stripe's Meter Events API + */ + public function getMeterEventsBase() + { + return $this->config['meter_events_base']; + } + /** * Gets the app info for this client. * @@ -157,12 +174,21 @@ public function getAppInfo() */ public function request($method, $path, $params, $opts) { - $opts = $this->defaultOpts->merge($opts, true); + $defaultRequestOpts = $this->defaultOpts; + $apiMode = \Stripe\Util\Util::getApiMode($path); + + $opts = $defaultRequestOpts->merge($opts, true); + $baseUrl = $opts->apiBase ?: $this->getApiBase(); $requestor = new \Stripe\ApiRequestor($this->apiKeyForRequest($opts), $baseUrl, $this->getAppInfo()); - list($response, $opts->apiKey) = $requestor->request($method, $path, $params, $opts->headers, 'standard', ['stripe_client']); + list($response, $opts->apiKey) = $requestor->request($method, $path, $params, $opts->headers, $apiMode, ['stripe_client']); $opts->discardNonPersistentHeaders(); - $obj = \Stripe\Util\Util::convertToStripeObject($response->json, $opts); + $obj = \Stripe\Util\Util::convertToStripeObject($response->json, $opts, $apiMode); + if (\is_array($obj)) { + // Edge case for v2 endpoints that return empty/void response + // Example: client->v2->billing->meterEventStream->create + $obj = new \Stripe\StripeObject(); + } $obj->setLastResponse($response); return $obj; @@ -185,12 +211,8 @@ public function rawRequest($method, $path, $params = null, $opts = []) if ('post' !== $method && null !== $params) { throw new Exception\InvalidArgumentException('Error: rawRequest only supports $params on post requests. Please pass null and add your parameters to $path'); } - $apiMode = 'standard'; + $apiMode = \Stripe\Util\Util::getApiMode($path); $headers = []; - if (\is_array($opts) && \array_key_exists('api_mode', $opts)) { - $apiMode = $opts['api_mode']; - unset($opts['api_mode']); - } if (\is_array($opts) && \array_key_exists('headers', $opts)) { $headers = $opts['headers'] ?: []; unset($opts['headers']); @@ -201,9 +223,6 @@ public function rawRequest($method, $path, $params = null, $opts = []) } $defaultRawRequestOpts = $this->defaultOpts; - if ('preview' === $apiMode) { - $defaultRawRequestOpts = $defaultRawRequestOpts->merge(['stripe_version' => \Stripe\Util\ApiVersion::PREVIEW], true); - } $opts = $defaultRawRequestOpts->merge($opts, true); @@ -225,6 +244,7 @@ public function rawRequest($method, $path, $params = null, $opts = []) * @param callable $readBodyChunkCallable a function that will be called * @param array $params the parameters of the request * @param array|\Stripe\Util\RequestOptions $opts the special modifiers of the request + * * with chunks of bytes from the body if the request is successful */ public function requestStream($method, $path, $readBodyChunkCallable, $params, $opts) @@ -232,7 +252,8 @@ public function requestStream($method, $path, $readBodyChunkCallable, $params, $ $opts = $this->defaultOpts->merge($opts, true); $baseUrl = $opts->apiBase ?: $this->getApiBase(); $requestor = new \Stripe\ApiRequestor($this->apiKeyForRequest($opts), $baseUrl, $this->getAppInfo()); - list($response, $opts->apiKey) = $requestor->requestStream($method, $path, $readBodyChunkCallable, $params, $opts->headers, 'standard', ['stripe_client']); + $apiMode = \Stripe\Util\Util::getApiMode($path); + list($response, $opts->apiKey) = $requestor->requestStream($method, $path, $readBodyChunkCallable, $params, $opts->headers, $apiMode, ['stripe_client']); } /** @@ -243,18 +264,28 @@ public function requestStream($method, $path, $readBodyChunkCallable, $params, $ * @param array $params the parameters of the request * @param array|\Stripe\Util\RequestOptions $opts the special modifiers of the request * - * @return \Stripe\Collection of ApiResources + * @return \Stripe\Collection|\Stripe\V2\Collection of ApiResources */ public function requestCollection($method, $path, $params, $opts) { $obj = $this->request($method, $path, $params, $opts); - if (!($obj instanceof \Stripe\Collection)) { - $received_class = \get_class($obj); - $msg = "Expected to receive `Stripe\\Collection` object from Stripe API. Instead received `{$received_class}`."; - - throw new \Stripe\Exception\UnexpectedValueException($msg); + $apiMode = \Stripe\Util\Util::getApiMode($path); + if ('v1' === $apiMode) { + if (!($obj instanceof \Stripe\Collection)) { + $received_class = \get_class($obj); + $msg = "Expected to receive `Stripe\\Collection` object from Stripe API. Instead received `{$received_class}`."; + + throw new \Stripe\Exception\UnexpectedValueException($msg); + } + $obj->setFilters($params); + } else { + if (!($obj instanceof \Stripe\V2\Collection)) { + $received_class = \get_class($obj); + $msg = "Expected to receive `Stripe\\V2\\Collection` object from Stripe API. Instead received `{$received_class}`."; + + throw new \Stripe\Exception\UnexpectedValueException($msg); + } } - $obj->setFilters($params); return $obj; } @@ -339,6 +370,11 @@ private function validateConfig($config) throw new \Stripe\Exception\InvalidArgumentException('stripe_account must be null or a string'); } + // stripe_context + if (null !== $config['stripe_context'] && !\is_string($config['stripe_context'])) { + throw new \Stripe\Exception\InvalidArgumentException('stripe_context must be null or a string'); + } + // stripe_version if (null !== $config['stripe_version'] && !\is_string($config['stripe_version'])) { throw new \Stripe\Exception\InvalidArgumentException('stripe_version must be null or a string'); @@ -385,11 +421,46 @@ private function validateConfig($config) * Deserializes the raw JSON string returned by rawRequest into a similar class. * * @param string $json + * @param 'v1'|'v2' $apiMode * * @return \Stripe\StripeObject * */ - public function deserialize($json) + public function deserialize($json, $apiMode = 'v1') + { + return \Stripe\Util\Util::convertToStripeObject(\json_decode($json, true), [], $apiMode); + } + + /** + * Returns a V2\Events instance using the provided JSON payload. Throws an + * Exception\UnexpectedValueException if the payload is not valid JSON, and + * an Exception\SignatureVerificationException if the signature + * verification fails for any reason. + * + * @param string $payload the payload sent by Stripe + * @param string $sigHeader the contents of the signature header sent by + * Stripe + * @param string $secret secret used to generate the signature + * @param int $tolerance maximum difference allowed between the header's + * timestamp and the current time. Defaults to 300 seconds (5 min) + * + * @throws Exception\SignatureVerificationException if the verification fails + * @throws Exception\UnexpectedValueException if the payload is not valid JSON, + * + * @return \Stripe\ThinEvent + */ + public function parseThinEvent($payload, $sigHeader, $secret, $tolerance = Webhook::DEFAULT_TOLERANCE) { - return \Stripe\Util\Util::convertToStripeObject(\json_decode($json, true), []); + $eventData = Util::utf8($payload); + WebhookSignature::verifyHeader($payload, $sigHeader, $secret, $tolerance); + + try { + return Util::json_decode_thin_event_object( + $eventData, + '\Stripe\ThinEvent' + ); + } catch (\ReflectionException $e) { + // Fail gracefully + return new \Stripe\ThinEvent(); + } } } diff --git a/lib/BaseStripeClientInterface.php b/lib/BaseStripeClientInterface.php index 6b004573f..dc3ec714c 100644 --- a/lib/BaseStripeClientInterface.php +++ b/lib/BaseStripeClientInterface.php @@ -41,4 +41,11 @@ public function getConnectBase(); * @return string the base URL for Stripe's Files API */ public function getFilesBase(); + + /** + * Gets the base URL for Stripe's Meter Events API. + * + * @return string the base URL for Stripe's Meter Events API + */ + public function getMeterEventsBase(); } diff --git a/lib/Billing/Alert.php b/lib/Billing/Alert.php index 5846efb1a..5545d5f19 100644 --- a/lib/Billing/Alert.php +++ b/lib/Billing/Alert.php @@ -10,11 +10,10 @@ * @property string $id Unique identifier for the object. * @property string $object String representing the object's type. Objects of the same type share the same value. * @property string $alert_type Defines the type of the alert. - * @property null|\Stripe\StripeObject $filter Limits the scope of the alert to a specific customer. * @property bool $livemode Has the value true if the object exists in live mode or the value false if the object exists in test mode. * @property null|string $status Status of the alert. This can be active, inactive or archived. * @property string $title Title of the alert. - * @property null|\Stripe\StripeObject $usage_threshold_config Encapsulates configuration of the alert to monitor usage on a specific Billing Meter. + * @property null|\Stripe\StripeObject $usage_threshold Encapsulates configuration of the alert to monitor usage on a specific Billing Meter. */ class Alert extends \Stripe\ApiResource { diff --git a/lib/Billing/CreditBalanceSummary.php b/lib/Billing/CreditBalanceSummary.php new file mode 100644 index 000000000..618fb1356 --- /dev/null +++ b/lib/Billing/CreditBalanceSummary.php @@ -0,0 +1,36 @@ +true if the object exists in live mode or the value false if the object exists in test mode. + */ +class CreditBalanceSummary extends \Stripe\SingletonApiResource +{ + const OBJECT_NAME = 'billing.credit_balance_summary'; + + /** + * Retrieves the credit balance summary for a customer. + * + * @param null|array|string $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Billing\CreditBalanceSummary + */ + public static function retrieve($opts = null) + { + $opts = \Stripe\Util\RequestOptions::parse($opts); + $instance = new static(null, $opts); + $instance->refresh(); + + return $instance; + } +} diff --git a/lib/Billing/CreditBalanceTransaction.php b/lib/Billing/CreditBalanceTransaction.php new file mode 100644 index 000000000..efd2abcfb --- /dev/null +++ b/lib/Billing/CreditBalanceTransaction.php @@ -0,0 +1,63 @@ +credit. + * @property string|\Stripe\Billing\CreditGrant $credit_grant The credit grant associated with this balance transaction. + * @property null|\Stripe\StripeObject $debit Debit details for this balance transaction. Only present if type is debit. + * @property int $effective_at The effective time of this balance transaction. + * @property bool $livemode Has the value true if the object exists in live mode or the value false if the object exists in test mode. + * @property null|string|\Stripe\TestHelpers\TestClock $test_clock ID of the test clock this credit balance transaction belongs to. + * @property null|string $type The type of balance transaction (credit or debit). + */ +class CreditBalanceTransaction extends \Stripe\ApiResource +{ + const OBJECT_NAME = 'billing.credit_balance_transaction'; + + const TYPE_CREDIT = 'credit'; + const TYPE_DEBIT = 'debit'; + + /** + * Retrieve a list of credit balance transactions. + * + * @param null|array $params + * @param null|array|string $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Collection<\Stripe\Billing\CreditBalanceTransaction> of ApiResources + */ + public static function all($params = null, $opts = null) + { + $url = static::classUrl(); + + return static::_requestPage($url, \Stripe\Collection::class, $params, $opts); + } + + /** + * Retrieves a credit balance transaction. + * + * @param array|string $id the ID of the API resource to retrieve, or an options array containing an `id` key + * @param null|array|string $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Billing\CreditBalanceTransaction + */ + public static function retrieve($id, $opts = null) + { + $opts = \Stripe\Util\RequestOptions::parse($opts); + $instance = new static($id, $opts); + $instance->refresh(); + + return $instance; + } +} diff --git a/lib/Billing/CreditGrant.php b/lib/Billing/CreditGrant.php new file mode 100644 index 000000000..76d91eb07 --- /dev/null +++ b/lib/Billing/CreditGrant.php @@ -0,0 +1,149 @@ +true if the object exists in live mode or the value false if the object exists in test mode. + * @property \Stripe\StripeObject $metadata Set of key-value pairs that you can attach to an object. This can be useful for storing additional information about the object in a structured format. + * @property null|string $name A descriptive name shown in dashboard and on invoices. + * @property null|string|\Stripe\TestHelpers\TestClock $test_clock ID of the test clock this credit grant belongs to. + * @property int $updated Time at which the object was last updated. Measured in seconds since the Unix epoch. + * @property null|int $voided_at The time when this credit grant was voided. If not present, the credit grant hasn't been voided. + */ +class CreditGrant extends \Stripe\ApiResource +{ + const OBJECT_NAME = 'billing.credit_grant'; + + use \Stripe\ApiOperations\Update; + + const CATEGORY_PAID = 'paid'; + const CATEGORY_PROMOTIONAL = 'promotional'; + + /** + * Creates a credit grant. + * + * @param null|array $params + * @param null|array|string $options + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Billing\CreditGrant the created resource + */ + public static function create($params = null, $options = null) + { + self::_validateParams($params); + $url = static::classUrl(); + + list($response, $opts) = static::_staticRequest('post', $url, $params, $options); + $obj = \Stripe\Util\Util::convertToStripeObject($response->json, $opts); + $obj->setLastResponse($response); + + return $obj; + } + + /** + * Retrieve a list of credit grants. + * + * @param null|array $params + * @param null|array|string $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Collection<\Stripe\Billing\CreditGrant> of ApiResources + */ + public static function all($params = null, $opts = null) + { + $url = static::classUrl(); + + return static::_requestPage($url, \Stripe\Collection::class, $params, $opts); + } + + /** + * Retrieves a credit grant. + * + * @param array|string $id the ID of the API resource to retrieve, or an options array containing an `id` key + * @param null|array|string $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Billing\CreditGrant + */ + public static function retrieve($id, $opts = null) + { + $opts = \Stripe\Util\RequestOptions::parse($opts); + $instance = new static($id, $opts); + $instance->refresh(); + + return $instance; + } + + /** + * Updates a credit grant. + * + * @param string $id the ID of the resource to update + * @param null|array $params + * @param null|array|string $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Billing\CreditGrant the updated resource + */ + public static function update($id, $params = null, $opts = null) + { + self::_validateParams($params); + $url = static::resourceUrl($id); + + list($response, $opts) = static::_staticRequest('post', $url, $params, $opts); + $obj = \Stripe\Util\Util::convertToStripeObject($response->json, $opts); + $obj->setLastResponse($response); + + return $obj; + } + + /** + * @param null|array $params + * @param null|array|string $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Billing\CreditGrant the expired credit grant + */ + public function expire($params = null, $opts = null) + { + $url = $this->instanceUrl() . '/expire'; + list($response, $opts) = $this->_request('post', $url, $params, $opts); + $this->refreshFrom($response, $opts); + + return $this; + } + + /** + * @param null|array $params + * @param null|array|string $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Billing\CreditGrant the voided credit grant + */ + public function voidGrant($params = null, $opts = null) + { + $url = $this->instanceUrl() . '/void'; + list($response, $opts) = $this->_request('post', $url, $params, $opts); + $this->refreshFrom($response, $opts); + + return $this; + } +} diff --git a/lib/Capability.php b/lib/Capability.php index d11df358f..056c5e814 100644 --- a/lib/Capability.php +++ b/lib/Capability.php @@ -16,7 +16,7 @@ * @property bool $requested Whether the capability has been requested. * @property null|int $requested_at Time at which the capability was requested. Measured in seconds since the Unix epoch. * @property null|\Stripe\StripeObject $requirements - * @property string $status The status of the capability. Can be active, inactive, pending, or unrequested. + * @property string $status The status of the capability. */ class Capability extends ApiResource { diff --git a/lib/CreditNoteLineItem.php b/lib/CreditNoteLineItem.php index 1df1f8bb3..bf4dafdfd 100644 --- a/lib/CreditNoteLineItem.php +++ b/lib/CreditNoteLineItem.php @@ -16,6 +16,7 @@ * @property \Stripe\StripeObject[] $discount_amounts The amount of discount calculated per discount for this line item * @property null|string $invoice_line_item ID of the invoice line item being credited * @property bool $livemode Has the value true if the object exists in live mode or the value false if the object exists in test mode. + * @property null|\Stripe\StripeObject[] $pretax_credit_amounts * @property null|int $quantity The number of units of product being credited. * @property \Stripe\StripeObject[] $tax_amounts The amount of tax calculated per tax rate for this line item * @property \Stripe\TaxRate[] $tax_rates The tax rates which apply to the line item. diff --git a/lib/Customer.php b/lib/Customer.php index ef337f701..1c7d246fa 100644 --- a/lib/Customer.php +++ b/lib/Customer.php @@ -5,9 +5,8 @@ namespace Stripe; /** - * This object represents a customer of your business. Use it to create recurring charges and track payments that belong to the same customer. - * - * Related guide: Save a card during payment + * This object represents a customer of your business. Use it to create recurring charges, save payment and contact information, + * and track payments that belong to the same customer. * * @property string $id Unique identifier for the object. * @property string $object String representing the object's type. Objects of the same type share the same value. diff --git a/lib/ErrorObject.php b/lib/ErrorObject.php index dbb640a0a..d032ab7d0 100644 --- a/lib/ErrorObject.php +++ b/lib/ErrorObject.php @@ -229,8 +229,9 @@ class ErrorObject extends StripeObject * @param array $values * @param null|array|string|Util\RequestOptions $opts * @param bool $partial defaults to false + * @param 'v1'|'v2' $apiMode */ - public function refreshFrom($values, $opts, $partial = false) + public function refreshFrom($values, $opts, $partial = false, $apiMode = 'v1') { // Unlike most other API resources, the API will omit attributes in // error objects when they have a null value. We manually set default diff --git a/lib/EventData/V1BillingMeterErrorReportTriggeredEventData.php b/lib/EventData/V1BillingMeterErrorReportTriggeredEventData.php new file mode 100644 index 000000000..3c8535c12 --- /dev/null +++ b/lib/EventData/V1BillingMeterErrorReportTriggeredEventData.php @@ -0,0 +1,15 @@ +data when fetched from /v2/events. + * @property \Stripe\StripeObject $reason This contains information about why meter error happens. + * @property int $validation_end The end of the window that is encapsulated by this summary. + * @property int $validation_start The start of the window that is encapsulated by this summary. + */ +class V1BillingMeterErrorReportTriggeredEventData extends \Stripe\StripeObject +{ +} diff --git a/lib/EventData/V1BillingMeterNoMeterFoundEventData.php b/lib/EventData/V1BillingMeterNoMeterFoundEventData.php new file mode 100644 index 000000000..3c1d70a50 --- /dev/null +++ b/lib/EventData/V1BillingMeterNoMeterFoundEventData.php @@ -0,0 +1,15 @@ +data when fetched from /v2/events. + * @property \Stripe\StripeObject $reason This contains information about why meter error happens. + * @property int $validation_end The end of the window that is encapsulated by this summary. + * @property int $validation_start The start of the window that is encapsulated by this summary. + */ +class V1BillingMeterNoMeterFoundEventData extends \Stripe\StripeObject +{ +} diff --git a/lib/Events/V1BillingMeterErrorReportTriggeredEvent.php b/lib/Events/V1BillingMeterErrorReportTriggeredEvent.php new file mode 100644 index 000000000..baf95f333 --- /dev/null +++ b/lib/Events/V1BillingMeterErrorReportTriggeredEvent.php @@ -0,0 +1,35 @@ +_request( + 'get', + $this->related_object->url, + [], + ['stripe_account' => $this->context], + [], + 'v2' + ); + + return \Stripe\Util\Util::convertToStripeObject($object, $options, 'v2'); + } +} diff --git a/lib/Events/V1BillingMeterNoMeterFoundEvent.php b/lib/Events/V1BillingMeterNoMeterFoundEvent.php new file mode 100644 index 000000000..39528eaf2 --- /dev/null +++ b/lib/Events/V1BillingMeterNoMeterFoundEvent.php @@ -0,0 +1,13 @@ + 0)) { - if (!$this->hasHeader($headers, 'Idempotency-Key')) { - $headers[] = 'Idempotency-Key: ' . $this->randomGenerator->uuid(); + // this is a little verbose, but makes v1 vs v2 behavior really clear + if (!$this->hasHeader($headers, 'Idempotency-Key')) { + // all v2 requests should have an IK + if ('v2' === $apiMode) { + if ('post' === $method || 'delete' === $method) { + $headers[] = 'Idempotency-Key: ' . $this->randomGenerator->uuid(); + } + } else { + // v1 requests should keep old behavior for consistency + if ('post' === $method && Stripe::$maxNetworkRetries > 0) { + $headers[] = 'Idempotency-Key: ' . $this->randomGenerator->uuid(); + } } } @@ -306,7 +320,7 @@ private function constructCurlOptions($method, $absUrl, $headers, $body, $opts) * @param array $headers * @param array $params * @param bool $hasFile - * @param 'preview'|'standard' $apiMode + * @param 'v1'|'v2' $apiMode */ private function constructRequest($method, $absUrl, $headers, $params, $hasFile, $apiMode) { @@ -314,7 +328,7 @@ private function constructRequest($method, $absUrl, $headers, $params, $hasFile, $opts = $this->calculateDefaultOptions($method, $absUrl, $headers, $params, $hasFile); list($absUrl, $body) = $this->constructUrlAndBody($method, $absUrl, $params, $hasFile, $apiMode); - $opts = $this->constructCurlOptions($method, $absUrl, $headers, $body, $opts); + $opts = $this->constructCurlOptions($method, $absUrl, $headers, $body, $opts, $apiMode); return [$opts, $absUrl]; } @@ -325,9 +339,9 @@ private function constructRequest($method, $absUrl, $headers, $params, $hasFile, * @param array $headers * @param array $params * @param bool $hasFile - * @param 'preview'|'standard' $apiMode + * @param 'v1'|'v2' $apiMode */ - public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode = 'standard') + public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode = 'v1') { list($opts, $absUrl) = $this->constructRequest($method, $absUrl, $headers, $params, $hasFile, $apiMode); list($rbody, $rcode, $rheaders) = $this->executeRequestWithRetries($opts, $absUrl); @@ -342,9 +356,9 @@ public function request($method, $absUrl, $headers, $params, $hasFile, $apiMode * @param array $params * @param bool $hasFile * @param callable $readBodyChunk - * @param 'preview'|'standard' $apiMode + * @param 'v1'|'v2' $apiMode */ - public function requestStream($method, $absUrl, $headers, $params, $hasFile, $readBodyChunk, $apiMode = 'standard') + public function requestStream($method, $absUrl, $headers, $params, $hasFile, $readBodyChunk, $apiMode = 'v1') { list($opts, $absUrl) = $this->constructRequest($method, $absUrl, $headers, $params, $hasFile, $apiMode); $opts[\CURLOPT_RETURNTRANSFER] = false; @@ -438,15 +452,7 @@ public function executeStreamingRequestWithRetries($opts, $absUrl, $readBodyChun $errno = null; $message = null; - $determineWriteCallback = function ($rheaders) use ( - &$readBodyChunk, - &$shouldRetry, - &$rbody, - &$numRetries, - &$rcode, - &$lastRHeaders, - &$errno - ) { + $determineWriteCallback = function ($rheaders) use (&$readBodyChunk, &$shouldRetry, &$rbody, &$numRetries, &$rcode, &$lastRHeaders, &$errno) { $lastRHeaders = $rheaders; $errno = \curl_errno($this->curlHandle); @@ -600,24 +606,24 @@ private function handleCurlError($url, $errno, $message, $numRetries) case \CURLE_COULDNT_RESOLVE_HOST: case \CURLE_OPERATION_TIMEOUTED: $msg = "Could not connect to Stripe ({$url}). Please check your " - . 'internet connection and try again. If this problem persists, ' - . "you should check Stripe's service status at " - . 'https://twitter.com/stripestatus, or'; + . 'internet connection and try again. If this problem persists, ' + . "you should check Stripe's service status at " + . 'https://twitter.com/stripestatus, or'; break; case \CURLE_SSL_CACERT: case \CURLE_SSL_PEER_CERTIFICATE: $msg = "Could not verify Stripe's SSL certificate. Please make sure " - . 'that your network is not intercepting certificates. ' - . "(Try going to {$url} in your browser.) " - . 'If this problem persists,'; + . 'that your network is not intercepting certificates. ' + . "(Try going to {$url} in your browser.) " + . 'If this problem persists,'; break; default: $msg = 'Unexpected error communicating with Stripe. ' - . 'If this problem persists,'; + . 'If this problem persists,'; } $msg .= ' let us know at support@stripe.com.'; diff --git a/lib/InvoiceLineItem.php b/lib/InvoiceLineItem.php index cfcea5055..d2bba32cf 100644 --- a/lib/InvoiceLineItem.php +++ b/lib/InvoiceLineItem.php @@ -26,6 +26,7 @@ * @property \Stripe\StripeObject $metadata Set of key-value pairs that you can attach to an object. This can be useful for storing additional information about the object in a structured format. Note that for line items with type=subscription, metadata reflects the current metadata from the subscription associated with the line item, unless the invoice line was directly updated with different metadata after creation. * @property \Stripe\StripeObject $period * @property null|\Stripe\Plan $plan The plan of the subscription, if the line item is a subscription or a proration. + * @property null|\Stripe\StripeObject[] $pretax_credit_amounts * @property null|\Stripe\Price $price The price of the line item. * @property bool $proration Whether this is a proration. * @property null|\Stripe\StripeObject $proration_details Additional details for proration line items diff --git a/lib/OAuthErrorObject.php b/lib/OAuthErrorObject.php index 620c5bb27..7190ac9b1 100644 --- a/lib/OAuthErrorObject.php +++ b/lib/OAuthErrorObject.php @@ -16,8 +16,9 @@ class OAuthErrorObject extends StripeObject * @param array $values * @param null|array|string|Util\RequestOptions $opts * @param bool $partial defaults to false + * @param 'v1'|'v2' $apiMode */ - public function refreshFrom($values, $opts, $partial = false) + public function refreshFrom($values, $opts, $partial = false, $apiMode = 'v1') { // Unlike most other API resources, the API will omit attributes in // error objects when they have a null value. We manually set default diff --git a/lib/Preview.php b/lib/Preview.php deleted file mode 100644 index 3b0a7ef07..000000000 --- a/lib/Preview.php +++ /dev/null @@ -1,36 +0,0 @@ -client = $client; - } - - private function getDefaultOpts($opts) - { - return \array_merge(['api_mode' => 'preview'], $opts); - } - - public function get($path, $opts = []) - { - return $this->client->rawRequest('get', $path, null, $this->getDefaultOpts($opts)); - } - - public function post($path, $params, $opts = []) - { - return $this->client->rawRequest('post', $path, $params, $this->getDefaultOpts($opts)); - } - - public function delete($path, $opts = []) - { - return $this->client->rawRequest('delete', $path, null, $this->getDefaultOpts($opts)); - } -} diff --git a/lib/PromotionCode.php b/lib/PromotionCode.php index 48a22b213..317555e13 100644 --- a/lib/PromotionCode.php +++ b/lib/PromotionCode.php @@ -11,7 +11,7 @@ * @property string $id Unique identifier for the object. * @property string $object String representing the object's type. Objects of the same type share the same value. * @property bool $active Whether the promotion code is currently active. A promotion code is only active if the coupon is also valid. - * @property string $code The customer-facing code. Regardless of case, this code must be unique across all active promotion codes for each customer. + * @property string $code The customer-facing code. Regardless of case, this code must be unique across all active promotion codes for each customer. Valid characters are lower case letters (a-z), upper case letters (A-Z), and digits (0-9). * @property \Stripe\Coupon $coupon A coupon contains information about a percent-off or amount-off discount you might want to apply to a customer. Coupons may be applied to subscriptions, invoices, checkout sessions, quotes, and more. Coupons do not work with conventional one-off charges or payment intents. * @property int $created Time at which the object was created. Measured in seconds since the Unix epoch. * @property null|string|\Stripe\Customer $customer The customer that this promotion code can be used by. diff --git a/lib/Reason.php b/lib/Reason.php new file mode 100644 index 000000000..36e65fc47 --- /dev/null +++ b/lib/Reason.php @@ -0,0 +1,13 @@ + */ - private $services; + use ServiceNavigatorTrait; /** * @param \Stripe\StripeClientInterface $client @@ -26,44 +22,5 @@ abstract class AbstractServiceFactory public function __construct($client) { $this->client = $client; - $this->services = []; - } - - /** - * @param string $name - * - * @return null|string - */ - abstract protected function getServiceClass($name); - - /** - * @param string $name - * - * @return null|AbstractService|AbstractServiceFactory - */ - public function __get($name) - { - return $this->getService($name); - } - - /** - * @param string $name - * - * @return null|AbstractService|AbstractServiceFactory - */ - public function getService($name) - { - $serviceClass = $this->getServiceClass($name); - if (null !== $serviceClass) { - if (!\array_key_exists($name, $this->services)) { - $this->services[$name] = new $serviceClass($this->client); - } - - return $this->services[$name]; - } - - \trigger_error('Undefined property: ' . static::class . '::$' . $name); - - return null; } } diff --git a/lib/Service/Billing/BillingServiceFactory.php b/lib/Service/Billing/BillingServiceFactory.php index 809febe62..eb1b160ed 100644 --- a/lib/Service/Billing/BillingServiceFactory.php +++ b/lib/Service/Billing/BillingServiceFactory.php @@ -8,6 +8,9 @@ * Service factory class for API resources in the Billing namespace. * * @property AlertService $alerts + * @property CreditBalanceSummaryService $creditBalanceSummary + * @property CreditBalanceTransactionService $creditBalanceTransactions + * @property CreditGrantService $creditGrants * @property MeterEventAdjustmentService $meterEventAdjustments * @property MeterEventService $meterEvents * @property MeterService $meters @@ -19,6 +22,9 @@ class BillingServiceFactory extends \Stripe\Service\AbstractServiceFactory */ private static $classMap = [ 'alerts' => AlertService::class, + 'creditBalanceSummary' => CreditBalanceSummaryService::class, + 'creditBalanceTransactions' => CreditBalanceTransactionService::class, + 'creditGrants' => CreditGrantService::class, 'meterEventAdjustments' => MeterEventAdjustmentService::class, 'meterEvents' => MeterEventService::class, 'meters' => MeterService::class, diff --git a/lib/Service/Billing/CreditBalanceSummaryService.php b/lib/Service/Billing/CreditBalanceSummaryService.php new file mode 100644 index 000000000..6169f05fd --- /dev/null +++ b/lib/Service/Billing/CreditBalanceSummaryService.php @@ -0,0 +1,27 @@ +request('get', '/v1/billing/credit_balance_summary', $params, $opts); + } +} diff --git a/lib/Service/Billing/CreditBalanceTransactionService.php b/lib/Service/Billing/CreditBalanceTransactionService.php new file mode 100644 index 000000000..b4b840877 --- /dev/null +++ b/lib/Service/Billing/CreditBalanceTransactionService.php @@ -0,0 +1,43 @@ + + */ + public function all($params = null, $opts = null) + { + return $this->requestCollection('get', '/v1/billing/credit_balance_transactions', $params, $opts); + } + + /** + * Retrieves a credit balance transaction. + * + * @param string $id + * @param null|array $params + * @param null|RequestOptionsArray|\Stripe\Util\RequestOptions $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Billing\CreditBalanceTransaction + */ + public function retrieve($id, $params = null, $opts = null) + { + return $this->request('get', $this->buildPath('/v1/billing/credit_balance_transactions/%s', $id), $params, $opts); + } +} diff --git a/lib/Service/Billing/CreditGrantService.php b/lib/Service/Billing/CreditGrantService.php new file mode 100644 index 000000000..29cbb1553 --- /dev/null +++ b/lib/Service/Billing/CreditGrantService.php @@ -0,0 +1,106 @@ + + */ + public function all($params = null, $opts = null) + { + return $this->requestCollection('get', '/v1/billing/credit_grants', $params, $opts); + } + + /** + * Creates a credit grant. + * + * @param null|array $params + * @param null|RequestOptionsArray|\Stripe\Util\RequestOptions $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Billing\CreditGrant + */ + public function create($params = null, $opts = null) + { + return $this->request('post', '/v1/billing/credit_grants', $params, $opts); + } + + /** + * Expires a credit grant. + * + * @param string $id + * @param null|array $params + * @param null|RequestOptionsArray|\Stripe\Util\RequestOptions $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Billing\CreditGrant + */ + public function expire($id, $params = null, $opts = null) + { + return $this->request('post', $this->buildPath('/v1/billing/credit_grants/%s/expire', $id), $params, $opts); + } + + /** + * Retrieves a credit grant. + * + * @param string $id + * @param null|array $params + * @param null|RequestOptionsArray|\Stripe\Util\RequestOptions $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Billing\CreditGrant + */ + public function retrieve($id, $params = null, $opts = null) + { + return $this->request('get', $this->buildPath('/v1/billing/credit_grants/%s', $id), $params, $opts); + } + + /** + * Updates a credit grant. + * + * @param string $id + * @param null|array $params + * @param null|RequestOptionsArray|\Stripe\Util\RequestOptions $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Billing\CreditGrant + */ + public function update($id, $params = null, $opts = null) + { + return $this->request('post', $this->buildPath('/v1/billing/credit_grants/%s', $id), $params, $opts); + } + + /** + * Voids a credit grant. + * + * @param string $id + * @param null|array $params + * @param null|RequestOptionsArray|\Stripe\Util\RequestOptions $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\Billing\CreditGrant + */ + public function voidGrant($id, $params = null, $opts = null) + { + return $this->request('post', $this->buildPath('/v1/billing/credit_grants/%s/void', $id), $params, $opts); + } +} diff --git a/lib/Service/CoreServiceFactory.php b/lib/Service/CoreServiceFactory.php index bd47bd342..b92511f09 100644 --- a/lib/Service/CoreServiceFactory.php +++ b/lib/Service/CoreServiceFactory.php @@ -79,6 +79,7 @@ * @property TopupService $topups * @property TransferService $transfers * @property Treasury\TreasuryServiceFactory $treasury + * @property V2\V2ServiceFactory $v2 * @property WebhookEndpointService $webhookEndpoints * // Doc: The end of the section generated from our OpenAPI spec */ @@ -162,6 +163,7 @@ class CoreServiceFactory extends \Stripe\Service\AbstractServiceFactory 'topups' => TopupService::class, 'transfers' => TransferService::class, 'treasury' => Treasury\TreasuryServiceFactory::class, + 'v2' => V2\V2ServiceFactory::class, 'webhookEndpoints' => WebhookEndpointService::class, // Class Map: The end of the section generated from our OpenAPI spec ]; diff --git a/lib/Service/ServiceNavigatorTrait.php b/lib/Service/ServiceNavigatorTrait.php new file mode 100644 index 000000000..c53f3721c --- /dev/null +++ b/lib/Service/ServiceNavigatorTrait.php @@ -0,0 +1,58 @@ + */ + protected $services = []; + + /** @var \Stripe\StripeClientInterface */ + protected $client; + + protected function getServiceClass($name) + { + \trigger_error('Undefined property: ' . static::class . '::$' . $name); + } + + public function __get($name) + { + $serviceClass = $this->getServiceClass($name); + if (null !== $serviceClass) { + if (!\array_key_exists($name, $this->services)) { + $this->services[$name] = new $serviceClass($this->client); + } + + return $this->services[$name]; + } + + \trigger_error('Undefined property: ' . static::class . '::$' . $name); + + return null; + } + + /** + * @param string $name + * + * @return null|AbstractService|AbstractServiceFactory + */ + public function getService($name) + { + $serviceClass = $this->getServiceClass($name); + if (null !== $serviceClass) { + if (!\array_key_exists($name, $this->services)) { + $this->services[$name] = new $serviceClass($this->client); + } + + return $this->services[$name]; + } + + \trigger_error('Undefined property: ' . static::class . '::$' . $name); + + return null; + } +} diff --git a/lib/Service/SubscriptionService.php b/lib/Service/SubscriptionService.php index fae6df831..7cbf5b9fe 100644 --- a/lib/Service/SubscriptionService.php +++ b/lib/Service/SubscriptionService.php @@ -27,23 +27,22 @@ public function all($params = null, $opts = null) } /** - * Cancels a customer’s subscription immediately. The customer will not be charged - * again for the subscription. - * - * Note, however, that any pending invoice items that you’ve created will still be - * charged for at the end of the period, unless manually deleted. If you’ve set the subscription to cancel - * at the end of the period, any pending prorations will also be left in place and - * collected at the end of the period. But if the subscription is set to cancel - * immediately, pending prorations will be removed. - * - * By default, upon subscription cancellation, Stripe will stop automatic - * collection of all finalized invoices for the customer. This is intended to - * prevent unexpected payment attempts after the customer has canceled a - * subscription. However, you can resume automatic collection of the invoices - * manually after subscription cancellation to have us proceed. Or, you could check - * for unpaid invoices before allowing the customer to cancel the subscription at - * all. + * Cancels a customer’s subscription immediately. The customer won’t be charged + * again for the subscription. After it’s canceled, you can no longer update the + * subscription or its metadata. + * + * Any pending invoice items that you’ve created are still charged at the end of + * the period, unless manually deleted. If you’ve + * set the subscription to cancel at the end of the period, any pending prorations + * are also left in place and collected at the end of the period. But if the + * subscription is set to cancel immediately, pending prorations are removed. + * + * By default, upon subscription cancellation, Stripe stops automatic collection of + * all finalized invoices for the customer. This is intended to prevent unexpected + * payment attempts after the customer has canceled a subscription. However, you + * can resume automatic collection of the invoices manually after subscription + * cancellation to have us proceed. Or, you could check for unpaid invoices before + * allowing the customer to cancel the subscription at all. * * @param string $id * @param null|array $params diff --git a/lib/Service/V2/Billing/BillingServiceFactory.php b/lib/Service/V2/Billing/BillingServiceFactory.php new file mode 100644 index 000000000..d24e45c2e --- /dev/null +++ b/lib/Service/V2/Billing/BillingServiceFactory.php @@ -0,0 +1,31 @@ + + */ + private static $classMap = [ + 'meterEventAdjustments' => MeterEventAdjustmentService::class, + 'meterEvents' => MeterEventService::class, + 'meterEventSession' => MeterEventSessionService::class, + 'meterEventStream' => MeterEventStreamService::class, + ]; + + protected function getServiceClass($name) + { + return \array_key_exists($name, self::$classMap) ? self::$classMap[$name] : null; + } +} diff --git a/lib/Service/V2/Billing/MeterEventAdjustmentService.php b/lib/Service/V2/Billing/MeterEventAdjustmentService.php new file mode 100644 index 000000000..c3c542e97 --- /dev/null +++ b/lib/Service/V2/Billing/MeterEventAdjustmentService.php @@ -0,0 +1,27 @@ +request('post', '/v2/billing/meter_event_adjustments', $params, $opts); + } +} diff --git a/lib/Service/V2/Billing/MeterEventService.php b/lib/Service/V2/Billing/MeterEventService.php new file mode 100644 index 000000000..7a13a19a2 --- /dev/null +++ b/lib/Service/V2/Billing/MeterEventService.php @@ -0,0 +1,29 @@ +request('post', '/v2/billing/meter_events', $params, $opts); + } +} diff --git a/lib/Service/V2/Billing/MeterEventSessionService.php b/lib/Service/V2/Billing/MeterEventSessionService.php new file mode 100644 index 000000000..d1ca99dab --- /dev/null +++ b/lib/Service/V2/Billing/MeterEventSessionService.php @@ -0,0 +1,29 @@ +request('post', '/v2/billing/meter_event_session', $params, $opts); + } +} diff --git a/lib/Service/V2/Billing/MeterEventStreamService.php b/lib/Service/V2/Billing/MeterEventStreamService.php new file mode 100644 index 000000000..e4ae2b7f8 --- /dev/null +++ b/lib/Service/V2/Billing/MeterEventStreamService.php @@ -0,0 +1,33 @@ +apiBase)) { + $opts->apiBase = $this->getClient()->getMeterEventsBase(); + } + $this->request('post', '/v2/billing/meter_event_stream', $params, $opts); + } +} diff --git a/lib/Service/V2/Core/CoreServiceFactory.php b/lib/Service/V2/Core/CoreServiceFactory.php new file mode 100644 index 000000000..7387b1203 --- /dev/null +++ b/lib/Service/V2/Core/CoreServiceFactory.php @@ -0,0 +1,27 @@ + + */ + private static $classMap = [ + // Class Map: The beginning of the section generated from our OpenAPI spec + 'events' => EventService::class, + // Class Map: The end of the section generated from our OpenAPI spec + ]; + + protected function getServiceClass($name) + { + return \array_key_exists($name, self::$classMap) ? self::$classMap[$name] : null; + } +} diff --git a/lib/Service/V2/Core/EventService.php b/lib/Service/V2/Core/EventService.php new file mode 100644 index 000000000..fdc5aaaa5 --- /dev/null +++ b/lib/Service/V2/Core/EventService.php @@ -0,0 +1,43 @@ + + */ + public function all($params = null, $opts = null) + { + return $this->requestCollection('get', '/v2/core/events', $params, $opts); + } + + /** + * Retrieves the details of an event. + * + * @param string $id + * @param null|array $params + * @param null|RequestOptionsArray|\Stripe\Util\RequestOptions $opts + * + * @throws \Stripe\Exception\ApiErrorException if the request fails + * + * @return \Stripe\V2\Event + */ + public function retrieve($id, $params = null, $opts = null) + { + return $this->request('get', $this->buildPath('/v2/core/events/%s', $id), $params, $opts); + } +} diff --git a/lib/Service/V2/V2ServiceFactory.php b/lib/Service/V2/V2ServiceFactory.php new file mode 100644 index 000000000..f16fe6238 --- /dev/null +++ b/lib/Service/V2/V2ServiceFactory.php @@ -0,0 +1,27 @@ + + */ + private static $classMap = [ + 'billing' => Billing\BillingServiceFactory::class, + 'core' => Core\CoreServiceFactory::class, + ]; + + protected function getServiceClass($name) + { + return \array_key_exists($name, self::$classMap) ? self::$classMap[$name] : null; + } +} diff --git a/lib/Stripe.php b/lib/Stripe.php index 5bee406f7..3984b4fff 100644 --- a/lib/Stripe.php +++ b/lib/Stripe.php @@ -43,12 +43,18 @@ class Stripe */ public static $logger = null; + // this is set higher (to `2`) in all other SDKs, but PHP gets a special exception + // because PHP scripts are run as short one-offs rather than long-lived servers. + // We didn't want to risk messing up integrations by setting a higher default + // since that would have worse side effects than other more long-running languages. /** @var int Maximum number of request retries */ public static $maxNetworkRetries = 0; /** @var bool Whether client telemetry is enabled. Defaults to true. */ public static $enableTelemetry = true; + // this is 5s in other languages + // see note on `maxNetworkRetries` for more info /** @var float Maximum delay between retries, in seconds */ private static $maxNetworkRetryDelay = 2.0; diff --git a/lib/StripeClient.php b/lib/StripeClient.php index c0d37bba3..83c7020f8 100644 --- a/lib/StripeClient.php +++ b/lib/StripeClient.php @@ -79,6 +79,7 @@ * @property \Stripe\Service\TopupService $topups * @property \Stripe\Service\TransferService $transfers * @property \Stripe\Service\Treasury\TreasuryServiceFactory $treasury + * @property \Stripe\Service\V2\V2ServiceFactory $v2 * @property \Stripe\Service\WebhookEndpointService $webhookEndpoints * // The end of the section generated from our OpenAPI spec */ diff --git a/lib/StripeObject.php b/lib/StripeObject.php index 40b175323..7e973755c 100644 --- a/lib/StripeObject.php +++ b/lib/StripeObject.php @@ -179,11 +179,11 @@ public function &__get($k) $class = static::class; $attrs = \implode(', ', \array_keys($this->_values)); $message = "Stripe Notice: Undefined property of {$class} instance: {$k}. " - . "HINT: The {$k} attribute was set in the past, however. " - . 'It was then wiped when refreshing the object ' - . "with the result returned by Stripe's API, " - . 'probably as a result of a save(). The attributes currently ' - . "available on this object are: {$attrs}"; + . "HINT: The {$k} attribute was set in the past, however. " + . 'It was then wiped when refreshing the object ' + . "with the result returned by Stripe's API, " + . 'probably as a result of a save(). The attributes currently ' + . "available on this object are: {$attrs}"; Stripe::getLogger()->error($message); return $nullval; @@ -266,13 +266,14 @@ public function values() * * @param array $values * @param null|array|string|Util\RequestOptions $opts + * @param 'v1'|'v2' $apiMode * * @return static the object constructed from the given values */ - public static function constructFrom($values, $opts = null) + public static function constructFrom($values, $opts = null, $apiMode = 'v1') { $obj = new static(isset($values['id']) ? $values['id'] : null); - $obj->refreshFrom($values, $opts); + $obj->refreshFrom($values, $opts, false, $apiMode); return $obj; } @@ -283,8 +284,9 @@ public static function constructFrom($values, $opts = null) * @param array $values * @param null|array|string|Util\RequestOptions $opts * @param bool $partial defaults to false + * @param 'v1'|'v2' $apiMode */ - public function refreshFrom($values, $opts, $partial = false) + public function refreshFrom($values, $opts, $partial = false, $apiMode = 'v1') { $this->_opts = Util\RequestOptions::parse($opts); @@ -307,7 +309,7 @@ public function refreshFrom($values, $opts, $partial = false) unset($this->{$k}); } - $this->updateAttributes($values, $opts, false); + $this->updateAttributes($values, $opts, false, $apiMode); foreach ($values as $k => $v) { $this->_transientValues->discard($k); $this->_unsavedValues->discard($k); @@ -320,8 +322,9 @@ public function refreshFrom($values, $opts, $partial = false) * @param array $values * @param null|array|string|Util\RequestOptions $opts * @param bool $dirty defaults to true + * @param 'v1'|'v2' $apiMode */ - public function updateAttributes($values, $opts = null, $dirty = true) + public function updateAttributes($values, $opts = null, $dirty = true, $apiMode = 'v1') { foreach ($values as $k => $v) { // Special-case metadata to always be cast as a StripeObject @@ -329,9 +332,9 @@ public function updateAttributes($values, $opts = null, $dirty = true) // not differentiate between lists and hashes, and we consider // empty arrays to be lists. if (('metadata' === $k) && (\is_array($v))) { - $this->_values[$k] = StripeObject::constructFrom($v, $opts); + $this->_values[$k] = StripeObject::constructFrom($v, $opts, $apiMode); } else { - $this->_values[$k] = Util\Util::convertToStripeObject($v, $opts); + $this->_values[$k] = Util\Util::convertToStripeObject($v, $opts, $apiMode); } if ($dirty) { $this->dirtyValue($this->_values[$k]); @@ -419,8 +422,8 @@ public function serializeParamsValue($value, $original, $unsaved, $force, $key = throw new Exception\InvalidArgumentException( "Cannot save property `{$key}` containing an API resource of type " . - \get_class($value) . ". It doesn't appear to be persisted and is " . - 'not marked as `saveWithParent`.' + \get_class($value) . ". It doesn't appear to be persisted and is " . + 'not marked as `saveWithParent`.' ); } if (\is_array($value)) { diff --git a/lib/Tax/Settings.php b/lib/Tax/Settings.php index 6da6e3d66..16dcdc2ea 100644 --- a/lib/Tax/Settings.php +++ b/lib/Tax/Settings.php @@ -13,7 +13,7 @@ * @property \Stripe\StripeObject $defaults * @property null|\Stripe\StripeObject $head_office The place where your business is located. * @property bool $livemode Has the value true if the object exists in live mode or the value false if the object exists in test mode. - * @property string $status The active status indicates you have all required settings to calculate tax. A status can transition out of active when new required settings are introduced. + * @property string $status The status of the Tax Settings. * @property \Stripe\StripeObject $status_details */ class Settings extends \Stripe\SingletonApiResource diff --git a/lib/ThinEvent.php b/lib/ThinEvent.php new file mode 100644 index 000000000..0ff3a2374 --- /dev/null +++ b/lib/ThinEvent.php @@ -0,0 +1,27 @@ +v2->core->events->retrieve(thin_event.id)` to fetch the full event object. + * + * @property string $id Unique identifier for the event. + * @property string $type The type of the event. + * @property string $created Time at which the object was created. + * @property null|string $context Authentication context needed to fetch the event or related object. + * @property null|RelatedObject $related_object Object containing the reference to API resource relevant to the event. + * @property null|Reason $reason Reason for the event. + * @property bool $livemode Livemode indicates if the event is from a production(true) or test(false) account. + */ +class ThinEvent +{ + public $id; + public $type; + public $created; + public $context; + public $related_object; + public $reason; + public $livemode; +} diff --git a/lib/Treasury/ReceivedCredit.php b/lib/Treasury/ReceivedCredit.php index 4f2e2ebb7..ce72e0431 100644 --- a/lib/Treasury/ReceivedCredit.php +++ b/lib/Treasury/ReceivedCredit.php @@ -31,6 +31,7 @@ class ReceivedCredit extends \Stripe\ApiResource const FAILURE_CODE_ACCOUNT_CLOSED = 'account_closed'; const FAILURE_CODE_ACCOUNT_FROZEN = 'account_frozen'; + const FAILURE_CODE_INTERNATIONAL_TRANSACTION = 'international_transaction'; const FAILURE_CODE_OTHER = 'other'; const NETWORK_ACH = 'ach'; diff --git a/lib/Util/EventTypes.php b/lib/Util/EventTypes.php new file mode 100644 index 000000000..8badd284c --- /dev/null +++ b/lib/Util/EventTypes.php @@ -0,0 +1,13 @@ + \Stripe\Events\V1BillingMeterErrorReportTriggeredEvent::class, + \Stripe\Events\V1BillingMeterNoMeterFoundEvent::LOOKUP_TYPE => \Stripe\Events\V1BillingMeterNoMeterFoundEvent::class, + // The end of the section generated from our OpenAPI spec + ]; +} diff --git a/lib/Util/ObjectTypes.php b/lib/Util/ObjectTypes.php index 75a24449a..b0a011e43 100644 --- a/lib/Util/ObjectTypes.php +++ b/lib/Util/ObjectTypes.php @@ -28,6 +28,9 @@ class ObjectTypes \Stripe\BankAccount::OBJECT_NAME => \Stripe\BankAccount::class, \Stripe\Billing\Alert::OBJECT_NAME => \Stripe\Billing\Alert::class, \Stripe\Billing\AlertTriggered::OBJECT_NAME => \Stripe\Billing\AlertTriggered::class, + \Stripe\Billing\CreditBalanceSummary::OBJECT_NAME => \Stripe\Billing\CreditBalanceSummary::class, + \Stripe\Billing\CreditBalanceTransaction::OBJECT_NAME => \Stripe\Billing\CreditBalanceTransaction::class, + \Stripe\Billing\CreditGrant::OBJECT_NAME => \Stripe\Billing\CreditGrant::class, \Stripe\Billing\Meter::OBJECT_NAME => \Stripe\Billing\Meter::class, \Stripe\Billing\MeterErrorReport::OBJECT_NAME => \Stripe\Billing\MeterErrorReport::class, \Stripe\Billing\MeterEvent::OBJECT_NAME => \Stripe\Billing\MeterEvent::class, @@ -172,4 +175,20 @@ class ObjectTypes \Stripe\WebhookEndpoint::OBJECT_NAME => \Stripe\WebhookEndpoint::class, // object classes: The end of the section generated from our OpenAPI spec ]; + + /** + * @var array Mapping from v2 object types to resource classes + */ + const v2Mapping = [ + // V1 Class needed for fetching the right related object + // TODO: https://go/j/DEVSDK-2204 Make a more standardized fix in codegen for all languages + \Stripe\Billing\Meter::OBJECT_NAME => \Stripe\Billing\Meter::class, + + // v2 object classes: The beginning of the section generated from our OpenAPI spec + \Stripe\V2\Billing\MeterEvent::OBJECT_NAME => \Stripe\V2\Billing\MeterEvent::class, + \Stripe\V2\Billing\MeterEventAdjustment::OBJECT_NAME => \Stripe\V2\Billing\MeterEventAdjustment::class, + \Stripe\V2\Billing\MeterEventSession::OBJECT_NAME => \Stripe\V2\Billing\MeterEventSession::class, + \Stripe\V2\Event::OBJECT_NAME => \Stripe\V2\Event::class, + // v2 object classes: The end of the section generated from our OpenAPI spec + ]; } diff --git a/lib/Util/RequestOptions.php b/lib/Util/RequestOptions.php index 488d234cc..62412ebd2 100644 --- a/lib/Util/RequestOptions.php +++ b/lib/Util/RequestOptions.php @@ -3,8 +3,8 @@ namespace Stripe\Util; /** - * @phpstan-type RequestOptionsArray array{api_key?: string, idempotency_key?: string, stripe_account?: string, stripe_version?: string, api_base?: string } - * @psalm-type RequestOptionsArray = array{api_key?: string, idempotency_key?: string, stripe_account?: string, stripe_version?: string, api_base?: string } + * @phpstan-type RequestOptionsArray array{api_key?: string, idempotency_key?: string, stripe_account?: string, stripe_context?: string, stripe_version?: string, api_base?: string } + * @psalm-type RequestOptionsArray = array{api_key?: string, idempotency_key?: string, stripe_account?: string, stripe_context?: string, stripe_version?: string, api_base?: string } */ class RequestOptions { @@ -129,11 +129,21 @@ public static function parse($options, $strict = false) unset($options['idempotency_key']); } if (\array_key_exists('stripe_account', $options)) { - $headers['Stripe-Account'] = $options['stripe_account']; + if (null !== $options['stripe_account']) { + $headers['Stripe-Account'] = $options['stripe_account']; + } unset($options['stripe_account']); } + if (\array_key_exists('stripe_context', $options)) { + if (null !== $options['stripe_context']) { + $headers['Stripe-Context'] = $options['stripe_context']; + } + unset($options['stripe_context']); + } if (\array_key_exists('stripe_version', $options)) { - $headers['Stripe-Version'] = $options['stripe_version']; + if (null !== $options['stripe_version']) { + $headers['Stripe-Version'] = $options['stripe_version']; + } unset($options['stripe_version']); } if (\array_key_exists('api_base', $options)) { @@ -151,9 +161,9 @@ public static function parse($options, $strict = false) } $message = 'The second argument to Stripe API method calls is an ' - . 'optional per-request apiKey, which must be a string, or ' - . 'per-request options, which must be an array. (HINT: you can set ' - . 'a global apiKey by "Stripe::setApiKey()")'; + . 'optional per-request apiKey, which must be a string, or ' + . 'per-request options, which must be an array. (HINT: you can set ' + . 'a global apiKey by "Stripe::setApiKey()")'; throw new \Stripe\Exception\InvalidArgumentException($message); } diff --git a/lib/Util/Util.php b/lib/Util/Util.php index cc7a8a48f..f0ba9bb8f 100644 --- a/lib/Util/Util.php +++ b/lib/Util/Util.php @@ -36,35 +36,86 @@ public static function isList($array) /** * Converts a response from the Stripe API to the corresponding PHP object. * - * @param array $resp the response from the Stripe API - * @param array $opts + * @param array $resp the response from the Stripe API + * @param array|RequestOptions $opts + * @param 'v1'|'v2' $apiMode whether the response is from a v1 or v2 API * * @return array|StripeObject */ - public static function convertToStripeObject($resp, $opts) + public static function convertToStripeObject($resp, $opts, $apiMode = 'v1') { - $types = \Stripe\Util\ObjectTypes::mapping; + $types = 'v1' === $apiMode ? \Stripe\Util\ObjectTypes::mapping + : \Stripe\Util\ObjectTypes::v2Mapping; if (self::isList($resp)) { $mapped = []; foreach ($resp as $i) { - $mapped[] = self::convertToStripeObject($i, $opts); + $mapped[] = self::convertToStripeObject($i, $opts, $apiMode); } return $mapped; } if (\is_array($resp)) { - if (isset($resp['object']) && \is_string($resp['object']) && isset($types[$resp['object']])) { + if (isset($resp['object']) && \is_string($resp['object']) + && isset($types[$resp['object']]) + ) { $class = $types[$resp['object']]; + if ('v2' === $apiMode && ('v2.core.event' === $resp['object'])) { + $eventTypes = \Stripe\Util\EventTypes::thinEventMapping; + if (\array_key_exists('type', $resp) && \array_key_exists($resp['type'], $eventTypes)) { + $class = $eventTypes[$resp['type']]; + } else { + $class = \Stripe\StripeObject::class; + } + } + } elseif (\array_key_exists('data', $resp) && \array_key_exists('next_page_url', $resp)) { + // TODO: this is a horrible hack. The API needs + // to return something for `object` here. + $class = \Stripe\V2\Collection::class; } else { $class = \Stripe\StripeObject::class; } - return $class::constructFrom($resp, $opts); + return $class::constructFrom($resp, $opts, $apiMode); } return $resp; } + /** + * @param mixed $json + * @param mixed $class + * + * @throws \ReflectionException + */ + public static function json_decode_thin_event_object($json, $class) + { + $reflection = new \ReflectionClass($class); + $instance = $reflection->newInstanceWithoutConstructor(); + $json = json_decode($json, true); + $properties = $reflection->getProperties(); + foreach ($properties as $key => $property) { + if (\array_key_exists($property->getName(), $json)) { + if ('related_object' === $property->getName()) { + $related_object = new \Stripe\RelatedObject(); + $related_object->id = $json['related_object']['id']; + $related_object->url = $json['related_object']['url']; + $related_object->type = $json['related_object']['type']; + $property->setValue($instance, $related_object); + } elseif ('reason' === $property->getName()) { + $reason = new \Stripe\Reason(); + $reason->id = $json['reason']['id']; + $reason->idempotency_key = $json['reason']['idempotency_key']; + $property->setValue($instance, $reason); + } else { + $property->setAccessible(true); + $property->setValue($instance, $json[$property->getName()]); + } + } + } + + return $instance; + } + /** * @param mixed|string $value a string to UTF8-encode * @@ -74,17 +125,25 @@ public static function convertToStripeObject($resp, $opts) public static function utf8($value) { if (null === self::$isMbstringAvailable) { - self::$isMbstringAvailable = \function_exists('mb_detect_encoding') && \function_exists('mb_convert_encoding'); + self::$isMbstringAvailable = \function_exists('mb_detect_encoding') + && \function_exists('mb_convert_encoding'); if (!self::$isMbstringAvailable) { - \trigger_error('It looks like the mbstring extension is not enabled. ' . - 'UTF-8 strings will not properly be encoded. Ask your system ' . - 'administrator to enable the mbstring extension, or write to ' . - 'support@stripe.com if you have any questions.', \E_USER_WARNING); + \trigger_error( + 'It looks like the mbstring extension is not enabled. ' . + 'UTF-8 strings will not properly be encoded. Ask your system ' + . + 'administrator to enable the mbstring extension, or write to ' + . + 'support@stripe.com if you have any questions.', + \E_USER_WARNING + ); } } - if (\is_string($value) && self::$isMbstringAvailable && 'UTF-8' !== \mb_detect_encoding($value, 'UTF-8', true)) { + if (\is_string($value) && self::$isMbstringAvailable + && 'UTF-8' !== \mb_detect_encoding($value, 'UTF-8', true) + ) { return mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1'); } @@ -160,12 +219,13 @@ public static function objectsToIds($h) /** * @param array $params + * @param mixed $apiMode * * @return string */ - public static function encodeParameters($params) + public static function encodeParameters($params, $apiMode = 'v1') { - $flattenedParams = self::flattenParams($params); + $flattenedParams = self::flattenParams($params, null, $apiMode); $pieces = []; foreach ($flattenedParams as $param) { list($k, $v) = $param; @@ -176,22 +236,31 @@ public static function encodeParameters($params) } /** - * @param array $params + * @param array $params * @param null|string $parentKey + * @param mixed $apiMode * * @return array */ - public static function flattenParams($params, $parentKey = null) - { + public static function flattenParams( + $params, + $parentKey = null, + $apiMode = 'v1' + ) { $result = []; foreach ($params as $key => $value) { $calculatedKey = $parentKey ? "{$parentKey}[{$key}]" : $key; - if (self::isList($value)) { - $result = \array_merge($result, self::flattenParamsList($value, $calculatedKey)); + $result = \array_merge( + $result, + self::flattenParamsList($value, $calculatedKey, $apiMode) + ); } elseif (\is_array($value)) { - $result = \array_merge($result, self::flattenParams($value, $calculatedKey)); + $result = \array_merge( + $result, + self::flattenParams($value, $calculatedKey, $apiMode) + ); } else { \array_push($result, [$calculatedKey, $value]); } @@ -201,22 +270,36 @@ public static function flattenParams($params, $parentKey = null) } /** - * @param array $value + * @param array $value * @param string $calculatedKey + * @param mixed $apiMode * * @return array */ - public static function flattenParamsList($value, $calculatedKey) - { + public static function flattenParamsList( + $value, + $calculatedKey, + $apiMode = 'v1' + ) { $result = []; foreach ($value as $i => $elem) { if (self::isList($elem)) { - $result = \array_merge($result, self::flattenParamsList($elem, $calculatedKey)); + $result = \array_merge( + $result, + self::flattenParamsList($elem, $calculatedKey) + ); } elseif (\is_array($elem)) { - $result = \array_merge($result, self::flattenParams($elem, "{$calculatedKey}[{$i}]")); + $result = \array_merge( + $result, + self::flattenParams($elem, "{$calculatedKey}[{$i}]") + ); } else { - \array_push($result, ["{$calculatedKey}[{$i}]", $elem]); + if ('v2' === $apiMode) { + \array_push($result, ["{$calculatedKey}", $elem]); + } else { + \array_push($result, ["{$calculatedKey}[{$i}]", $elem]); + } } } @@ -266,4 +349,14 @@ public static function currentTimeMillis() { return (int) \round(\microtime(true) * 1000); } + + public static function getApiMode($path) + { + $apiMode = 'v1'; + if ('/v2' === substr($path, 0, 3)) { + $apiMode = 'v2'; + } + + return $apiMode; + } } diff --git a/lib/V2/Billing/MeterEvent.php b/lib/V2/Billing/MeterEvent.php new file mode 100644 index 000000000..56009b321 --- /dev/null +++ b/lib/V2/Billing/MeterEvent.php @@ -0,0 +1,21 @@ +event_name field on a meter. + * @property string $identifier A unique identifier for the event. If not provided, one will be generated. We recommend using a globally unique identifier for this. We’ll enforce uniqueness within a rolling 24 hour period. + * @property bool $livemode Has the value true if the object exists in live mode or the value false if the object exists in test mode. + * @property \Stripe\StripeObject $payload The payload of the event. This must contain the fields corresponding to a meter’s customer_mapping.event_payload_key (default is stripe_customer_id) and value_settings.event_payload_key (default is value). Read more about the payload. + * @property int $timestamp The time of the event. Must be within the past 35 calendar days or up to 5 minutes in the future. Defaults to current timestamp if not specified. + */ +class MeterEvent extends \Stripe\ApiResource +{ + const OBJECT_NAME = 'billing.meter_event'; +} diff --git a/lib/V2/Billing/MeterEventAdjustment.php b/lib/V2/Billing/MeterEventAdjustment.php new file mode 100644 index 000000000..7f99059cd --- /dev/null +++ b/lib/V2/Billing/MeterEventAdjustment.php @@ -0,0 +1,23 @@ +event_name field on a meter. + * @property bool $livemode Has the value true if the object exists in live mode or the value false if the object exists in test mode. + * @property string $status Open Enum. The meter event adjustment’s status. + * @property string $type Open Enum. Specifies whether to cancel a single event or a range of events for a time period. Time period cancellation is not supported yet. + */ +class MeterEventAdjustment extends \Stripe\ApiResource +{ + const OBJECT_NAME = 'billing.meter_event_adjustment'; + + const STATUS_COMPLETE = 'complete'; + const STATUS_PENDING = 'pending'; +} diff --git a/lib/V2/Billing/MeterEventSession.php b/lib/V2/Billing/MeterEventSession.php new file mode 100644 index 000000000..15f0b761b --- /dev/null +++ b/lib/V2/Billing/MeterEventSession.php @@ -0,0 +1,18 @@ +true if the object exists in live mode or the value false if the object exists in test mode. + */ +class MeterEventSession extends \Stripe\ApiResource +{ + const OBJECT_NAME = 'billing.meter_event_session'; +} diff --git a/lib/V2/Collection.php b/lib/V2/Collection.php new file mode 100644 index 000000000..d8a1ded6e --- /dev/null +++ b/lib/V2/Collection.php @@ -0,0 +1,110 @@ + + * + * @property null|string $next_page_url + * @property null|string $previous_page_url + * @property TStripeObject[] $data + */ +class Collection extends \Stripe\StripeObject implements \Countable, \IteratorAggregate +{ + const OBJECT_NAME = 'list'; + + use \Stripe\ApiOperations\Request; + + /** + * @return string the base URL for the given class + */ + public static function baseUrl() + { + return \Stripe\Stripe::$apiBase; + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet($k) + { + if (\is_string($k)) { + return parent::offsetGet($k); + } + $msg = "You tried to access the {$k} index, but V2Collection " . + 'types only support string keys. (HINT: List calls ' . + 'return an object with a `data` (which is the data ' . + "array). You likely want to call ->data[{$k}])"; + + throw new \Stripe\Exception\InvalidArgumentException($msg); + } + + /** + * @return int the number of objects in the current page + */ + #[\ReturnTypeWillChange] + public function count() + { + return \count($this->data); + } + + /** + * @return \ArrayIterator an iterator that can be used to iterate + * across objects in the current page + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + return new \ArrayIterator($this->data); + } + + /** + * @return \ArrayIterator an iterator that can be used to iterate + * backwards across objects in the current page + */ + public function getReverseIterator() + { + return new \ArrayIterator(\array_reverse($this->data)); + } + + /** + * @throws \Stripe\Exception\ApiErrorException + * + * @return \Generator|TStripeObject[] A generator that can be used to + * iterate across all objects across all pages. As page boundaries are + * encountered, the next page will be fetched automatically for + * continued iteration. + */ + public function autoPagingIterator() + { + $page = $this->data; + $next_page_url = $this->next_page_url; + + while (true) { + foreach ($page as $item) { + yield $item; + } + if (null === $next_page_url) { + break; + } + + list($response, $opts) = $this->_request( + 'get', + $next_page_url, + null, + null, + [], + 'v2' + ); + $obj = \Stripe\Util\Util::convertToStripeObject($response, $opts, 'v2'); + /** @phpstan-ignore-next-line */ + $page = $obj->data; + /** @phpstan-ignore-next-line */ + $next_page_url = $obj->next_page_url; + } + } +} diff --git a/lib/V2/Event.php b/lib/V2/Event.php new file mode 100644 index 000000000..a8ac4dbb5 --- /dev/null +++ b/lib/V2/Event.php @@ -0,0 +1,16 @@ +stubRequest( - 'POST', - '/v1/charges', + 'GET', + '/v2/core/events/evt_123', [], null, false, [ 'error' => [ - 'developer_message' => 'Unacceptable', + 'type' => 'temporary_session_expired', + 'code' => 'session_bad', + 'message' => 'you messed up', ], ], - 400 + 400, + BaseStripeClient::DEFAULT_API_BASE ); try { - Charge::create(); + $client = new StripeClient('sk_test_123'); + $client->v2->core->events->retrieve('evt_123'); + static::fail('Did not raise error'); + } catch (TemporarySessionExpiredException $e) { + static::assertSame(400, $e->getHttpStatus()); + static::assertSame('temporary_session_expired', $e->getError()->type); + static::assertSame('session_bad', $e->getStripeCode()); + static::assertSame('you messed up', $e->getMessage()); + } catch (\Exception $e) { + static::fail('Unexpected exception: ' . \get_class($e)); + } + } + + public function testV2CallsFallBackToV1Errors() + { + $this->stubRequest( + 'GET', + '/v2/core/events/evt_123', + [], + null, + false, + [ + 'error' => [ + 'code' => 'invalid_request', + 'message' => 'your request is invalid', + 'param' => 'invalid_param', + ], + ], + 400, + BaseStripeClient::DEFAULT_API_BASE + ); + + try { + $client = new StripeClient('sk_test_123'); + $client->v2->core->events->retrieve('evt_123'); static::fail('Did not raise error'); } catch (Exception\InvalidRequestException $e) { static::assertSame(400, $e->getHttpStatus()); - static::assertSame('Unacceptable', $e->getMessage()); + static::assertSame('invalid_param', $e->getStripeParam()); + static::assertSame('invalid_request', $e->getStripeCode()); + static::assertSame('your request is invalid', $e->getMessage()); } catch (\Exception $e) { static::fail('Unexpected exception: ' . \get_class($e)); } @@ -609,6 +649,50 @@ public function testHeaderStripeAccountRequestOptions() Charge::create([], ['stripe_account' => 'acct_123']); } + public function testHeaderNullStripeAccountRequestOptionsDoesntSendHeader() + { + $this->stubRequest( + 'POST', + '/v1/charges', + [], + function ($array) { + foreach ($array as $header) { + // polyfilled str_starts_with from https://gist.github.com/juliyvchirkov/8f325f9ac534fe736b504b93a1a8b2ce + if (0 === strpos(\strtolower($header), 'stripe-account')) { + return false; + } + } + + return true; + }, + false, + [ + 'id' => 'ch_123', + 'object' => 'charge', + ] + ); + Charge::create([], ['stripe_account' => null]); + } + + public function testHeaderStripeContextRequestOptions() + { + $this->stubRequest( + 'POST', + '/v2/billing/meter_event_session', + [], + [ + 'Stripe-Context: wksp_123', + ], + false, + ['object' => 'billing.meter_event_session'], + 200, + BaseStripeClient::DEFAULT_API_BASE + ); + + $client = new StripeClient('sk_test_123'); + $client->v2->billing->meterEventSession->create([], ['stripe_context' => 'wksp_123']); + } + public function testIsDisabled() { $reflector = new \ReflectionClass(\Stripe\ApiRequestor::class); diff --git a/tests/Stripe/BaseStripeClientTest.php b/tests/Stripe/BaseStripeClientTest.php index 1d7cec404..87ece8304 100644 --- a/tests/Stripe/BaseStripeClientTest.php +++ b/tests/Stripe/BaseStripeClientTest.php @@ -11,12 +11,15 @@ final class BaseStripeClientTest extends \Stripe\TestCase { use TestHelper; + /** @var \ReflectionProperty */ private $optsReflector; /** @var \ReflectionClass */ private $apiRequestorReflector; + private $curlClientStub; + protected function headerStartsWith($header, $name) { return substr($header, 0, \strlen($name)) === $name; @@ -35,6 +38,15 @@ protected function setUpApiRequestorReflector() $this->apiRequestorReflector = new \ReflectionClass(\Stripe\ApiRequestor::class); } + /** @before */ + protected function setUpCurlClientStub() + { + $this->curlClientStub = $this->getMockBuilder(\Stripe\HttpClient\CurlClient::class) + ->setMethods(['executeRequestWithRetries']) + ->getMock() + ; + } + public function testCtorDoesNotThrowWhenNoParams() { $client = new BaseStripeClient(); @@ -237,6 +249,308 @@ public function testRequestWithNoVersionDefaultsToPinnedVersion() ); } + public function testJsonRawRequestGetWithURLParams() + { + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{}', 200, []]) + ; + + $opts = null; + $this->curlClientStub->expects(static::once()) + ->method('executeRequestWithRetries') + ->with(static::callback(function ($opts_) use (&$opts) { + $opts = $opts_; + + return true; + }), MOCK_URL . '/v1/xyz?foo=bar') + ; + + ApiRequestor::setHttpClient($this->curlClientStub); + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'stripe_account' => 'acct_123', + 'api_base' => MOCK_URL, + ]); + $client->rawRequest('get', '/v1/xyz?foo=bar', null, []); + static::assertArrayNotHasKey(\CURLOPT_POST, $opts); + static::assertArrayNotHasKey(\CURLOPT_POSTFIELDS, $opts); + $content_type = null; + $stripe_version = null; + foreach ($opts[\CURLOPT_HTTPHEADER] as $header) { + if (self::headerStartsWith($header, 'Content-Type:')) { + $content_type = $header; + } + if (self::headerStartsWith($header, 'Stripe-Version:')) { + $stripe_version = $header; + } + } + // The library sends Content-Type even with no body, so assert this + // But it would be more correct to not send Content-Type + static::assertSame('Content-Type: application/x-www-form-urlencoded', $content_type); + static::assertSame('Stripe-Version: ' . ApiVersion::CURRENT, $stripe_version); + } + + public function testRawRequestUsageTelemetry() + { + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{}', 200, ['request-id' => 'req_123']]) + ; + + $this->curlClientStub->expects(static::once()) + ->method('executeRequestWithRetries') + ->with(static::callback(function ($opts) { + return true; + }), MOCK_URL . '/v1/xyz') + ; + ApiRequestor::setHttpClient($this->curlClientStub); + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'api_base' => MOCK_URL, + ]); + $client->rawRequest('post', '/v1/xyz', [], []); + // Can't use ->getStaticPropertyValue because this has a bug until PHP 7.4.9: https://bugs.php.net/bug.php?id=69804 + static::assertSame(['raw_request'], $this->apiRequestorReflector->getStaticProperties()['requestTelemetry']->usage); + } + + public function testJsonRawRequestPost() + { + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{"object": "xyz", "isPHPBestLanguage": true, "abc": {"object": "abc", "a": 2}}', 200, []]) + ; + + $this->curlClientStub->expects(static::once()) + ->method('executeRequestWithRetries') + ->with(static::callback(function ($opts) { + $this->assertSame(1, $opts[\CURLOPT_POST]); + $this->assertSame('{"foo":"bar","baz":{"qux":false}}', $opts[\CURLOPT_POSTFIELDS]); + $this->assertContains('Content-Type: application/json', $opts[\CURLOPT_HTTPHEADER]); + + return true; + }), MOCK_URL . '/v2/xyz') + ; + + ApiRequestor::setHttpClient($this->curlClientStub); + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'stripe_account' => 'acct_123', + 'api_base' => MOCK_URL, + ]); + $params = ['foo' => 'bar', 'baz' => ['qux' => false]]; + $resp = $client->rawRequest('post', '/v2/xyz', $params, []); + + $xyz = $client->deserialize($resp->body, 'v2'); + + static::assertSame('xyz', $xyz->object); // @phpstan-ignore-line + static::assertTrue($xyz->isPHPBestLanguage); // @phpstan-ignore-line + static::assertSame(2, $xyz->abc->a); // @phpstan-ignore-line + static::assertInstanceof(\Stripe\StripeObject::class, $xyz->abc); // @phpstan-ignore-line + } + + public function testFormRawRequestPost() + { + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{}', 200, []]) + ; + + $this->curlClientStub->expects(static::once()) + ->method('executeRequestWithRetries') + ->with(static::callback(function ($opts) { + $this->assertSame(1, $opts[\CURLOPT_POST]); + $this->assertSame('foo=bar&baz[qux]=false', $opts[\CURLOPT_POSTFIELDS]); + $this->assertContains('Content-Type: application/x-www-form-urlencoded', $opts[\CURLOPT_HTTPHEADER]); + + return true; + }), MOCK_URL . '/v1/xyz') + ; + + ApiRequestor::setHttpClient($this->curlClientStub); + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'stripe_account' => 'acct_123', + 'api_base' => MOCK_URL, + ]); + $params = ['foo' => 'bar', 'baz' => ['qux' => false]]; + $client->rawRequest('post', '/v1/xyz', $params, []); + } + + public function testJsonRawRequestGetWithNonNullParams() + { + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'stripe_account' => 'acct_123', + 'api_base' => MOCK_URL, + ]); + $params = []; + $this->expectException(\Stripe\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Error: rawRequest only supports $params on post requests. Please pass null and add your parameters to $path'); + $client->rawRequest('get', '/v2/xyz', $params, []); + } + + public function testRawRequestWithStripeContextOption() + { + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{}', 200, []]) + ; + + $this->curlClientStub->expects(static::once()) + ->method('executeRequestWithRetries') + ->with(static::callback(function ($opts) { + $this->assertContains('Stripe-Context: acct_123', $opts[\CURLOPT_HTTPHEADER]); + + return true; + }), MOCK_URL . '/v2/xyz') + ; + + ApiRequestor::setHttpClient($this->curlClientStub); + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'stripe_account' => 'acct_123', + 'api_base' => MOCK_URL, + ]); + $params = []; + $client->rawRequest('post', '/v2/xyz', $params, [ + 'stripe_context' => 'acct_123', + ]); + } + + public function testV2GetRequest() + { + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{"object": "billing.meter_event_session"}', 200, []]) + ; + + $this->curlClientStub->expects(static::once()) + ->method('executeRequestWithRetries') + ->with(static::callback(function ($opts) { + $this->assertSame(1, $opts[\CURLOPT_HTTPGET]); + + // The library sends Content-Type even with no body, so assert this + // But it would be more correct to not send Content-Type + $this->assertContains('Content-Type: application/json', $opts[\CURLOPT_HTTPHEADER]); + + return true; + }), MOCK_URL . '/v2/billing/meter_event_session') + ; + + ApiRequestor::setHttpClient($this->curlClientStub); + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'stripe_version' => '2222-22-22.preview-v2', + 'api_base' => MOCK_URL, + ]); + $meterEventSession = $client->request('get', '/v2/billing/meter_event_session', [], []); + static::assertNotNull($meterEventSession); + static::assertInstanceOf(\Stripe\V2\Billing\MeterEventSession::class, $meterEventSession); + } + + public function testV2PostRequest() + { + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{"object": "billing.meter_event_session"}', 200, []]) + ; + + $this->curlClientStub->expects(static::once()) + ->method('executeRequestWithRetries') + ->with(static::callback(function ($opts) { + $this->assertSame(1, $opts[\CURLOPT_POST]); + $this->assertSame('{"foo":"bar"}', $opts[\CURLOPT_POSTFIELDS]); + $this->assertContains('Content-Type: application/json', $opts[\CURLOPT_HTTPHEADER]); + + return true; + }), MOCK_URL . '/v2/billing/meter_event_session') + ; + + ApiRequestor::setHttpClient($this->curlClientStub); + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'stripe_version' => '2222-22-22.preview-v2', + 'api_base' => MOCK_URL, + ]); + + $meterEventSession = $client->request('post', '/v2/billing/meter_event_session', ['foo' => 'bar'], []); + static::assertNotNull($meterEventSession); + static::assertInstanceOf(\Stripe\V2\Billing\MeterEventSession::class, $meterEventSession); + } + + public function testV2PostRequestWithEmptyParams() + { + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{"object": "billing.meter_event_session"}', 200, []]) + ; + + $this->curlClientStub->expects(static::once()) + ->method('executeRequestWithRetries') + ->with(static::callback(function ($opts) { + $this->assertSame(1, $opts[\CURLOPT_POST]); + $this->assertArrayNotHasKey(\CURLOPT_POSTFIELDS, $opts); + $this->assertContains('Content-Type: application/json', $opts[\CURLOPT_HTTPHEADER]); + + return true; + }), MOCK_URL . '/v2/billing/meter_event_session') + ; + + ApiRequestor::setHttpClient($this->curlClientStub); + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'stripe_version' => '2222-22-22.preview-v2', + 'api_base' => MOCK_URL, + ]); + + $meterEventSession = $client->request('post', '/v2/billing/meter_event_session', [], []); + static::assertNotNull($meterEventSession); + static::assertInstanceOf(\Stripe\V2\Billing\MeterEventSession::class, $meterEventSession); + } + + public function testV2RequestWithClientStripeContext() + { + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{"object": "account"}', 200, []]) + ; + + $this->curlClientStub->expects(static::once()) + ->method('executeRequestWithRetries') + ->with(static::callback(function ($opts) { + $this->assertContains('Stripe-Context: acct_123', $opts[\CURLOPT_HTTPHEADER]); + + return true; + }), MOCK_URL . '/v2/accounts') + ; + + ApiRequestor::setHttpClient($this->curlClientStub); + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'stripe_context' => 'acct_123', + 'api_base' => MOCK_URL, + ]); + + $client->request('post', '/v2/accounts', [], []); + } + + public function testV2RequestWithOptsStripeContext() + { + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{"object": "account"}', 200, []]) + ; + + $this->curlClientStub->expects(static::once()) + ->method('executeRequestWithRetries') + ->with(static::callback(function ($opts) { + $this->assertContains('Stripe-Context: acct_456', $opts[\CURLOPT_HTTPHEADER]); + + return true; + }), MOCK_URL . '/v2/accounts') + ; + + ApiRequestor::setHttpClient($this->curlClientStub); + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'stripe_context' => 'acct_123', + 'api_base' => MOCK_URL, + ]); + + $client->request('post', '/v2/accounts', [], ['stripe_context' => 'acct_456']); + } + private function assertAppInfo($ua, $ua_dict, $headers) { static::assertContains($ua, $headers); @@ -406,242 +720,142 @@ public function testConfigValidationFindsExtraAppInfoKeys() ]); } - public function testJsonRawRequestGetWithURLParams() + public function testParseThinEvent() { - $curlClientStub = $this->getMockBuilder(\Stripe\HttpClient\CurlClient::class) - ->setMethods(['executeRequestWithRetries']) - ->getMock() - ; - $curlClientStub->method('executeRequestWithRetries') - ->willReturn(['{}', 200, []]) - ; + $jsonEvent = [ + 'id' => 'evt_234', + 'object' => 'event', + 'type' => 'financial_account.balance.opened', + 'created' => '2022-02-15T00:27:45.330Z', + 'related_object' => [ + 'id' => 'fa_123', + 'type' => 'financial_account', + 'url' => '/v2/financial_accounts/fa_123', + 'stripe_context' => 'acct_123', + ], + ]; - $opts = null; - $curlClientStub->expects(static::once()) - ->method('executeRequestWithRetries') - ->with(static::callback(function ($opts_) use (&$opts) { - $opts = $opts_; + $eventData = json_encode($jsonEvent); + $client = new BaseStripeClient(['api_key' => 'sk_test_client', 'api_base' => MOCK_URL, 'stripe_account' => 'acc_123']); - return true; - }), MOCK_URL . '/v1/xyz?foo=bar') - ; + $sigHeader = WebhookTest::generateHeader(['payload' => $eventData]); + $event = $client->parseThinEvent($eventData, $sigHeader, WebhookTest::SECRET); - ApiRequestor::setHttpClient($curlClientStub); - $client = new BaseStripeClient([ - 'api_key' => 'sk_test_client', - 'stripe_account' => 'acct_123', - 'api_base' => MOCK_URL, - ]); - $client->rawRequest('get', '/v1/xyz?foo=bar', null, []); - static::assertArrayNotHasKey(\CURLOPT_POST, $opts); - static::assertArrayNotHasKey(\CURLOPT_POSTFIELDS, $opts); - $content_type = null; - $stripe_version = null; - foreach ($opts[\CURLOPT_HTTPHEADER] as $header) { - if (self::headerStartsWith($header, 'Content-Type:')) { - $content_type = $header; - } - if (self::headerStartsWith($header, 'Stripe-Version:')) { - $stripe_version = $header; - } - } - // The library sends Content-Type even with no body, so assert this - // But it would be more correct to not send Content-Type - static::assertSame('Content-Type: application/x-www-form-urlencoded', $content_type); - static::assertSame('Stripe-Version: ' . ApiVersion::CURRENT, $stripe_version); + static::assertNotInstanceOf(\Stripe\StripeObject::class, $event); + static::assertSame('evt_234', $event->id); + static::assertSame('financial_account.balance.opened', $event->type); + static::assertSame('2022-02-15T00:27:45.330Z', $event->created); + static::assertSame('fa_123', $event->related_object->id); } - public function testRawRequestUsageTelemetry() + public function testV2OverridesPreviewVersionIfPassedInRawRequestOptions() { - $curlClientStub = $this->getMockBuilder(\Stripe\HttpClient\CurlClient::class) - ->setMethods(['executeRequestWithRetries']) - ->getMock() - ; - $curlClientStub->method('executeRequestWithRetries') - ->willReturn(['{}', 200, ['request-id' => 'req_123']]) + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{"object": "account"}', 200, []]) ; - $curlClientStub->expects(static::once()) + $this->curlClientStub->expects(static::once()) ->method('executeRequestWithRetries') ->with(static::callback(function ($opts) { + $this->assertContains('Stripe-Version: 2222-22-22.preview-v2', $opts[\CURLOPT_HTTPHEADER]); + return true; - }), MOCK_URL . '/v1/xyz') + }), MOCK_URL . '/v2/accounts/acct_123') ; - ApiRequestor::setHttpClient($curlClientStub); + + ApiRequestor::setHttpClient($this->curlClientStub); $client = new BaseStripeClient([ 'api_key' => 'sk_test_client', 'api_base' => MOCK_URL, ]); - $client->rawRequest('post', '/v1/xyz', [], [ - 'api_mode' => 'standard', + $params = []; + $client->rawRequest('post', '/v2/accounts/acct_123', $params, [ + 'stripe_version' => '2222-22-22.preview-v2', ]); - // Can't use ->getStaticPropertyValue because this has a bug until PHP 7.4.9: https://bugs.php.net/bug.php?id=69804 - static::assertSame(['raw_request'], $this->apiRequestorReflector->getStaticProperties()['requestTelemetry']->usage); } - public function testJsonRawRequestPost() + public function testV2OverridesPreviewVersionIfPassedInRequestOptions() { - $curlClientStub = $this->getMockBuilder(\Stripe\HttpClient\CurlClient::class) - ->setMethods(['executeRequestWithRetries']) - ->getMock() - ; - $curlClientStub->method('executeRequestWithRetries') - ->willReturn(['{"object": "xyz", "isPHPBestLanguage": true, "abc": {"object": "abc", "a": 2}}', 200, []]) + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{"object": "billing.meter_event_session"}', 200, []]) ; - $curlClientStub->expects(static::once()) + $this->curlClientStub->expects(static::once()) ->method('executeRequestWithRetries') ->with(static::callback(function ($opts) { - $this->assertSame(1, $opts[\CURLOPT_POST]); - $this->assertSame('{"foo":"bar","baz":{"qux":false}}', $opts[\CURLOPT_POSTFIELDS]); - $this->assertContains('Content-Type: application/json', $opts[\CURLOPT_HTTPHEADER]); + $this->assertContains('Stripe-Version: 2222-22-22.preview-v2', $opts[\CURLOPT_HTTPHEADER]); return true; - }), MOCK_URL . '/v1/xyz') + }), MOCK_URL . '/v2/billing/meter_event_session/bmes_123') ; - ApiRequestor::setHttpClient($curlClientStub); + ApiRequestor::setHttpClient($this->curlClientStub); $client = new BaseStripeClient([ 'api_key' => 'sk_test_client', - 'stripe_account' => 'acct_123', 'api_base' => MOCK_URL, ]); - $params = ['foo' => 'bar', 'baz' => ['qux' => false]]; - $resp = $client->rawRequest('post', '/v1/xyz', $params, [ - 'api_mode' => 'preview', - ]); - - $decoded = \json_decode($resp->body, true); - $xyz = \Stripe\StripeObject::constructFrom($decoded); - - static::assertSame('xyz', $xyz->object); // @phpstan-ignore-line - static::assertTrue($xyz->isPHPBestLanguage); // @phpstan-ignore-line - static::assertSame(2, $xyz->abc->a); // @phpstan-ignore-line - static::assertInstanceof(\Stripe\StripeObject::class, $xyz->abc); // @phpstan-ignore-line + $meterEventSession = $client->request('get', '/v2/billing/meter_event_session/bmes_123', [], ['stripe_version' => '2222-22-22.preview-v2']); + static::assertNotNull($meterEventSession); + static::assertInstanceOf(\Stripe\V2\Billing\MeterEventSession::class, $meterEventSession); } - public function testFormRawRequestPost() + public function testV1AndV2Request() { - $curlClientStub = $this->getMockBuilder(\Stripe\HttpClient\CurlClient::class) - ->setMethods(['executeRequestWithRetries']) - ->getMock() - ; - $curlClientStub->method('executeRequestWithRetries') - ->willReturn(['{}', 200, []]) + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturnOnConsecutiveCalls(['{"object": "billing.meter_event_session"}', 200, []], ['{"object": "billing.meter_event"}', 200, []]) ; - $curlClientStub->expects(static::once()) + $this->curlClientStub ->method('executeRequestWithRetries') - ->with(static::callback(function ($opts) { - $this->assertSame(1, $opts[\CURLOPT_POST]); - $this->assertSame('foo=bar&baz[qux]=false', $opts[\CURLOPT_POSTFIELDS]); - $this->assertContains('Content-Type: application/x-www-form-urlencoded', $opts[\CURLOPT_HTTPHEADER]); + ->withConsecutive([static::callback(function ($opts) { + $this->assertContains('Stripe-Version: ' . ApiVersion::CURRENT, $opts[\CURLOPT_HTTPHEADER]); return true; - }), MOCK_URL . '/v1/xyz') + }), MOCK_URL . '/v2/billing/meter_event_session/bmes_123'], [ + static::callback(function ($opts) { + $this->assertContains('Stripe-Version: ' . ApiVersion::CURRENT, $opts[\CURLOPT_HTTPHEADER]); + + return true; + }), MOCK_URL . '/v1/billing/meter_event/bmes_123', + ]) ; - ApiRequestor::setHttpClient($curlClientStub); + ApiRequestor::setHttpClient($this->curlClientStub); $client = new BaseStripeClient([ 'api_key' => 'sk_test_client', - 'stripe_account' => 'acct_123', 'api_base' => MOCK_URL, ]); - $params = ['foo' => 'bar', 'baz' => ['qux' => false]]; - $client->rawRequest('post', '/v1/xyz', $params, [ - 'api_mode' => 'standard', - ]); - } + $meterEventSession = $client->request('get', '/v2/billing/meter_event_session/bmes_123', [], []); + static::assertNotNull($meterEventSession); + static::assertInstanceOf(\Stripe\V2\Billing\MeterEventSession::class, $meterEventSession); - public function testJsonRawRequestGetWithNonNullParams() - { - $client = new BaseStripeClient([ - 'api_key' => 'sk_test_client', - 'stripe_account' => 'acct_123', - 'api_base' => MOCK_URL, - ]); - $params = []; - $this->expectException(\Stripe\Exception\InvalidArgumentException::class); - $this->expectExceptionMessage('Error: rawRequest only supports $params on post requests. Please pass null and add your parameters to $path'); - $client->rawRequest('get', '/v1/xyz', $params, [ - 'api_mode' => 'preview', - ]); + $meterEvent = $client->request('get', '/v1/billing/meter_event/bmes_123', [], []); + static::assertNotNull($meterEvent); + static::assertInstanceOf(\Stripe\Billing\MeterEvent::class, $meterEvent); } - public function testRawRequestWithStripeContextOption() + public function testV2RequestWithEmptyResponse() { - $curlClientStub = $this->getMockBuilder(\Stripe\HttpClient\CurlClient::class) - ->setMethods(['executeRequestWithRetries']) - ->getMock() - ; - $curlClientStub->method('executeRequestWithRetries') + $this->curlClientStub->method('executeRequestWithRetries') ->willReturn(['{}', 200, []]) ; - $curlClientStub->expects(static::once()) + $this->curlClientStub->expects(static::once()) ->method('executeRequestWithRetries') ->with(static::callback(function ($opts) { - $this->assertContains('Stripe-Context: acct_123', $opts[\CURLOPT_HTTPHEADER]); - return true; - }), MOCK_URL . '/v1/xyz') + }), MOCK_URL . '/v2/billing/meter_event_stream') ; - ApiRequestor::setHttpClient($curlClientStub); + ApiRequestor::setHttpClient($this->curlClientStub); $client = new BaseStripeClient([ 'api_key' => 'sk_test_client', - 'stripe_account' => 'acct_123', + 'stripe_version' => '2222-22-22.preview-v2', 'api_base' => MOCK_URL, ]); - $params = []; - $client->rawRequest('post', '/v1/xyz', $params, [ - 'api_mode' => 'preview', - 'stripe_context' => 'acct_123', - ]); - } - - public function testPreviewGetRequest() - { - $curlClientStub = $this->getMockBuilder(\Stripe\HttpClient\CurlClient::class) - ->setMethods(['executeRequestWithRetries']) - ->getMock() - ; - $curlClientStub->method('executeRequestWithRetries') - ->willReturn(['{}', 200, []]) - ; - - $opts = null; - $curlClientStub->expects(static::once()) - ->method('executeRequestWithRetries') - ->with(static::callback(function ($opts_) use (&$opts) { - $opts = $opts_; - return true; - }), MOCK_URL . '/v1/xyz?foo=bar') - ; - - ApiRequestor::setHttpClient($curlClientStub); - $client = new BaseStripeClient([ - 'api_key' => 'sk_test_client', - 'stripe_account' => 'acct_123', - 'api_base' => MOCK_URL, - ]); - $client->preview->get('/v1/xyz?foo=bar', []); - static::assertArrayNotHasKey(\CURLOPT_POST, $opts); - static::assertArrayNotHasKey(\CURLOPT_POSTFIELDS, $opts); - $content_type = null; - $stripe_version = null; - foreach ($opts[\CURLOPT_HTTPHEADER] as $header) { - if (self::headerStartsWith($header, 'Content-Type:')) { - $content_type = $header; - } - if (self::headerStartsWith($header, 'Stripe-Version:')) { - $stripe_version = $header; - } - } - // The library sends Content-Type even with no body, so assert this - // But it would be more correct to not send Content-Type - static::assertSame('Content-Type: application/json', $content_type); - static::assertSame('Stripe-Version: ' . \Stripe\Util\ApiVersion::PREVIEW, $stripe_version); + $meterEventStream = $client->request('post', '/v2/billing/meter_event_stream', [], []); + static::assertNotNull($meterEventStream); + static::assertInstanceOf(\Stripe\StripeObject::class, $meterEventStream); } } diff --git a/tests/Stripe/GeneratedExamplesTest.php b/tests/Stripe/GeneratedExamplesTest.php index 8f0f1ba98..dcf9ee3c5 100644 --- a/tests/Stripe/GeneratedExamplesTest.php +++ b/tests/Stripe/GeneratedExamplesTest.php @@ -3610,22 +3610,6 @@ public function testTerminalReadersProcessPaymentIntentPost() static::assertInstanceOf(\Stripe\Terminal\Reader::class, $result); } - public function testTerminalReadersProcessSetupIntentPost() - { - $this->expectsRequest( - 'post', - '/v1/terminal/readers/tmr_xxxxxxxxxxxxx/process_setup_intent' - ); - $result = $this->client->terminal->readers->processSetupIntent( - 'tmr_xxxxxxxxxxxxx', - [ - 'setup_intent' => 'seti_xxxxxxxxxxxxx', - 'customer_consent_collected' => true, - ] - ); - static::assertInstanceOf(\Stripe\Terminal\Reader::class, $result); - } - public function testTestHelpersCustomersFundCashBalancePost() { $this->expectsRequest( diff --git a/tests/Stripe/HttpClient/CurlClientTest.php b/tests/Stripe/HttpClient/CurlClientTest.php index 9554c4515..faa8aa0c4 100644 --- a/tests/Stripe/HttpClient/CurlClientTest.php +++ b/tests/Stripe/HttpClient/CurlClientTest.php @@ -35,6 +35,9 @@ final class CurlClientTest extends \Stripe\TestCase /** @var \ReflectionMethod */ private $shouldRetryMethod; + /** @var \ReflectionMethod */ + private $constructCurlOptionsMethod; + /** * @before */ @@ -68,6 +71,9 @@ public function setUpReflectors() $this->curlHandle = $curlClientReflector->getProperty('curlHandle'); $this->curlHandle->setAccessible(true); + + $this->constructCurlOptionsMethod = $curlClientReflector->getMethod('constructCurlOptions'); + $this->constructCurlOptionsMethod->setAccessible(true); } /** @@ -337,6 +343,89 @@ public function testSleepTimeShouldAddSomeRandomness() static::assertSame($baseValue * 8, $this->sleepTimeMethod->invoke($curlClient, 4, [])); } + /** + * Checks if a list of headers contains a specific header name. Copied from CurlClient. + * + * @param string[] $headers + * @param string $name + * + * @return bool + */ + private function hasHeader($headers, $name) + { + foreach ($headers as $header) { + if (0 === \strncasecmp($header, "{$name}: ", \strlen($name) + 2)) { + return true; + } + } + + return false; + } + + public function testIdempotencyKeyV2PostRequestsNoRetry() + { + \Stripe\Stripe::setMaxNetworkRetries(0); + $curlClient = new CurlClient(); + $curlOpts = $this->constructCurlOptionsMethod->invoke($curlClient, 'post', '', [], '', [], 'v2'); + $headers = $curlOpts[\CURLOPT_HTTPHEADER]; + static::assertTrue($this->hasHeader($headers, 'Idempotency-Key')); + } + + public function testIdempotencyKeyV2DeleteRequestsNoRetry() + { + \Stripe\Stripe::setMaxNetworkRetries(0); + $curlClient = new CurlClient(); + $curlOpts = $this->constructCurlOptionsMethod->invoke($curlClient, 'delete', '', [], '', [], 'v2'); + $headers = $curlOpts[\CURLOPT_HTTPHEADER]; + static::assertTrue($this->hasHeader($headers, 'Idempotency-Key')); + } + + public function testIdempotencyKeyAllV2RequestsWithRetry() + { + \Stripe\Stripe::setMaxNetworkRetries(3); + $curlClient = new CurlClient(); + $curlOpts = $this->constructCurlOptionsMethod->invoke($curlClient, 'post', '', [], '', [], 'v2'); + $headers = $curlOpts[\CURLOPT_HTTPHEADER]; + static::assertTrue($this->hasHeader($headers, 'Idempotency-Key')); + } + + // we don't want this behavior - write requests should basically always have an IK. But until we fix it, let's test it + public function testNoIdempotencyKeyV1PostRequestsNoRetry() + { + \Stripe\Stripe::setMaxNetworkRetries(0); + $curlClient = new CurlClient(); + $curlOpts = $this->constructCurlOptionsMethod->invoke($curlClient, 'post', '', [], '', [], 'v1'); + $headers = $curlOpts[\CURLOPT_HTTPHEADER]; + static::assertFalse($this->hasHeader($headers, 'Idempotency-Key')); + } + + public function testNoIdempotencyKeyV1DeleteRequestsNoRetry() + { + \Stripe\Stripe::setMaxNetworkRetries(0); + $curlClient = new CurlClient(); + $curlOpts = $this->constructCurlOptionsMethod->invoke($curlClient, 'delete', '', [], '', [], 'v1'); + $headers = $curlOpts[\CURLOPT_HTTPHEADER]; + static::assertFalse($this->hasHeader($headers, 'Idempotency-Key')); + } + + public function testIdempotencyKeyV1PostRequestsWithRetry() + { + \Stripe\Stripe::setMaxNetworkRetries(3); + $curlClient = new CurlClient(); + $curlOpts = $this->constructCurlOptionsMethod->invoke($curlClient, 'post', '', [], '', [], 'v1'); + $headers = $curlOpts[\CURLOPT_HTTPHEADER]; + static::assertTrue($this->hasHeader($headers, 'Idempotency-Key')); + } + + public function testNoIdempotencyKeyV1DeleteRequestsWithRetry() + { + \Stripe\Stripe::setMaxNetworkRetries(3); + $curlClient = new CurlClient(); + $curlOpts = $this->constructCurlOptionsMethod->invoke($curlClient, 'delete', '', [], '', [], 'v1'); + $headers = $curlOpts[\CURLOPT_HTTPHEADER]; + static::assertFalse($this->hasHeader($headers, 'Idempotency-Key')); + } + public function testResponseHeadersCaseInsensitive() { $charge = \Stripe\Charge::all(); @@ -466,7 +555,8 @@ public function testExecuteStreamingRequestWithRetriesPersistentConnection() $opts[\CURLOPT_HTTPGET] = 1; $opts[\CURLOPT_URL] = $absUrl; $opts[\CURLOPT_HTTPHEADER] = ['Authorization: Basic c2tfdGVzdF94eXo6']; - $discardCallback = function ($chunk) {}; + $discardCallback = function ($chunk) { + }; $curl->executeStreamingRequestWithRetries($opts, $absUrl, $discardCallback); $firstHandle = $this->curlHandle->getValue($curl); diff --git a/tests/Stripe/PreviewTest.php b/tests/Stripe/PreviewTest.php deleted file mode 100644 index bfd42bd32..000000000 --- a/tests/Stripe/PreviewTest.php +++ /dev/null @@ -1,79 +0,0 @@ -client = new BaseStripeClient([ - 'api_key' => 'sk_test_client', - 'stripe_account' => 'acct_123', - 'api_base' => MOCK_URL, - ]); - } - - public function testPreviewGet() - { - $this->stubRequest( - 'GET', - '/v1/xyz?foo=bar', - [], - ['Content-Type: application/json', 'Stripe-Version: ' . \Stripe\Util\ApiVersion::PREVIEW] - ); - - $this->client->preview->get('/v1/xyz?foo=bar', []); - } - - public function testPreviewPost() - { - $this->stubRequest( - 'POST', - '/v1/xyz', - ['foo' => 'bar', 'baz' => ['qux' => false]], - ['Content-Type: application/json', 'Stripe-Version: ' . \Stripe\Util\ApiVersion::PREVIEW] - ); - - $params = ['foo' => 'bar', 'baz' => ['qux' => false]]; - $this->client->preview->post('/v1/xyz', $params, []); - } - - public function testPreviewDelete() - { - $this->stubRequest( - 'DELETE', - '/v1/xyz/xyz_123', - [], - ['Content-Type: application/json', 'Stripe-Version: ' . \Stripe\Util\ApiVersion::PREVIEW] - ); - - $this->client->preview->delete('/v1/xyz/xyz_123', []); - } - - public function testOverrideDefaultOoptions() - { - $stripeVersionOverride = '2022-11-15'; - $stripeContext = 'acct_123'; - - $this->stubRequest( - 'GET', - '/v1/xyz/xyz_123', - [], - ['Content-Type: application/json', 'Stripe-Version: ' . $stripeVersionOverride, 'Stripe-Context: ' . $stripeContext] - ); - - $this->client->preview->get('/v1/xyz/xyz_123', ['stripe_version' => $stripeVersionOverride, 'stripe_context' => $stripeContext]); - } -} diff --git a/tests/Stripe/StripeClientTest.php b/tests/Stripe/StripeClientTest.php index 75969e8c1..6e21768dc 100644 --- a/tests/Stripe/StripeClientTest.php +++ b/tests/Stripe/StripeClientTest.php @@ -8,11 +8,76 @@ */ final class StripeClientTest extends \Stripe\TestCase { + use \Stripe\TestHelper; + + /** @var \Stripe\StripeClient */ + private $client; + + /** + * @before + */ + public function setUpFixture() + { + $this->client = new StripeClient('sk_test_123'); + } + public function testExposesPropertiesForServices() { - $client = new StripeClient('sk_test_123'); - static::assertInstanceOf(\Stripe\Service\CouponService::class, $client->coupons); - static::assertInstanceOf(\Stripe\Service\Issuing\IssuingServiceFactory::class, $client->issuing); - static::assertInstanceOf(\Stripe\Service\Issuing\CardService::class, $client->issuing->cards); + static::assertInstanceOf(\Stripe\Service\CouponService::class, $this->client->coupons); + static::assertInstanceOf(\Stripe\Service\Issuing\IssuingServiceFactory::class, $this->client->issuing); + static::assertInstanceOf(\Stripe\Service\Issuing\CardService::class, $this->client->issuing->cards); + } + + public function testListMethodReturnsPageableCollection() + { + $curlClientStub = $this->getMockBuilder(\Stripe\HttpClient\CurlClient::class) + ->setMethods(['executeRequestWithRetries']) + ->getMock() + ; + + $curlClientStub->method('executeRequestWithRetries') + ->willReturnOnConsecutiveCalls([ + '{"data": [{"id": "evnt_123", "object": "v2.core.event", "type": "v1.billing.meter.no_meter_found"}, {"id": "evnt_456", "object": "v2.core.event", "type": "v1.billing.meter.no_meter_found"}], "next_page_url": "/v2/core/events?limit=2&page=page_2"}', + 200, + [], + ], [ + '{"data": [{"id": "evnt_789", "object": "v2.core.event", "type": "v1.billing.meter.no_meter_found"}], "next_page_url": null}', + 200, + [], + ]) + ; + + $cb = static::callback(function ($opts) { + $this->assertContains('Authorization: Bearer sk_test_client', $opts[\CURLOPT_HTTPHEADER]); + $this->assertContains('Content-Type: application/json', $opts[\CURLOPT_HTTPHEADER]); + + return true; + }); + + $curlClientStub->expects(static::exactly(2)) + ->method('executeRequestWithRetries') + ->withConsecutive( + [$cb, MOCK_URL . '/v2/core/events?limit=2'], + [$cb, MOCK_URL . '/v2/core/events?limit=2&page=page_2'] + ) + ; + + ApiRequestor::setHttpClient($curlClientStub); + + $client = new StripeClient([ + 'api_key' => 'sk_test_client', + 'api_base' => MOCK_URL, + ]); + + $events = $client->v2->core->events->all(['limit' => 2]); + static::assertInstanceOf(\Stripe\V2\Collection::class, $events); + static::assertInstanceOf(\Stripe\Events\V1BillingMeterNoMeterFoundEvent::class, $events->data[0]); + + $seen = []; + foreach ($events->autoPagingIterator() as $event) { + $seen[] = $event['id']; + } + + static::assertSame(['evnt_123', 'evnt_456', 'evnt_789'], $seen); } } diff --git a/tests/Stripe/Util/RequestOptionsTest.php b/tests/Stripe/Util/RequestOptionsTest.php index 3aca8c7a4..510eb3896 100644 --- a/tests/Stripe/Util/RequestOptionsTest.php +++ b/tests/Stripe/Util/RequestOptionsTest.php @@ -148,6 +148,7 @@ public function testDiscardNonPersistentHeaders() $opts = RequestOptions::parse( [ 'stripe_account' => 'foo', + 'stripe_context' => 'foo', 'idempotency_key' => 'foo', ] ); diff --git a/tests/Stripe/Util/UtilTest.php b/tests/Stripe/Util/UtilTest.php index c87b8040b..aaf43d168 100644 --- a/tests/Stripe/Util/UtilTest.php +++ b/tests/Stripe/Util/UtilTest.php @@ -2,6 +2,8 @@ namespace Stripe\Util; +use Stripe\ThinEvent; + /** * @internal * @covers \Stripe\Util\Util @@ -98,6 +100,26 @@ public function testEncodeParameters() ); } + public function testEncodeParametersForV2Api() + { + $params = [ + 'a' => 3, + 'b' => '+foo?', + 'c' => 'bar&baz', + 'd' => ['a' => 'a', 'b' => 'b'], + 'e' => [0, 1], + 'f' => '', + + // note the empty hash won't even show up in the request + 'g' => [], + ]; + + static::assertSame( + 'a=3&b=%2Bfoo%3F&c=bar%26baz&d[a]=a&d[b]=b&e=0&e=1&f=', + Util::encodeParameters($params, 'v2') + ); + } + public function testUrlEncode() { static::assertSame('foo', Util::urlEncode('foo')); @@ -137,4 +159,68 @@ public function testFlattenParams() Util::flattenParams($params) ); } + + public function testJsonDecodeThinEventObject() + { + $eventData = json_encode([ + 'id' => 'evt_234', + 'object' => 'event', + 'type' => 'financial_account.balance.opened', + 'created' => '2022-02-15T00:27:45.330Z', + 'related_object' => [ + 'id' => 'fa_123', + 'type' => 'financial_account', + 'url' => '/v2/financial_accounts/fa_123', + ], + 'reason' => [ + 'id' => 'id_123', + 'idempotency_key' => 'key_123', + ], + ]); + + $event = Util::json_decode_thin_event_object($eventData, ThinEvent::class); + static::assertInstanceOf(ThinEvent::class, $event); + static::assertSame('evt_234', $event->id); + static::assertSame('financial_account.balance.opened', $event->type); + static::assertSame('2022-02-15T00:27:45.330Z', $event->created); + static::assertSame('fa_123', $event->related_object->id); + static::assertSame('financial_account', $event->related_object->type); + static::assertSame('/v2/financial_accounts/fa_123', $event->related_object->url); + static::assertSame('id_123', $event->reason->id); + static::assertSame('key_123', $event->reason->idempotency_key); + } + + public function testJsonDecodeThinEventObjectWithNoRelatedObject() + { + $eventData = json_encode([ + 'id' => 'evt_234', + 'object' => 'event', + 'type' => 'financial_account.balance.opened', + 'created' => '2022-02-15T00:27:45.330Z', + ]); + + $event = Util::json_decode_thin_event_object($eventData, ThinEvent::class); + static::assertInstanceOf(ThinEvent::class, $event); + static::assertSame('evt_234', $event->id); + static::assertSame('financial_account.balance.opened', $event->type); + static::assertSame('2022-02-15T00:27:45.330Z', $event->created); + static::assertNull($event->related_object); + } + + public function testJsonDecodeThinEventObjectWithNoReasonObject() + { + $eventData = json_encode([ + 'id' => 'evt_234', + 'object' => 'event', + 'type' => 'financial_account.balance.opened', + 'created' => '2022-02-15T00:27:45.330Z', + ]); + + $event = Util::json_decode_thin_event_object($eventData, ThinEvent::class); + static::assertInstanceOf(ThinEvent::class, $event); + static::assertSame('evt_234', $event->id); + static::assertSame('financial_account.balance.opened', $event->type); + static::assertSame('2022-02-15T00:27:45.330Z', $event->created); + static::assertNull($event->reason); + } } diff --git a/tests/Stripe/V2/CollectionTest.php b/tests/Stripe/V2/CollectionTest.php new file mode 100644 index 000000000..0b8bf0c3b --- /dev/null +++ b/tests/Stripe/V2/CollectionTest.php @@ -0,0 +1,209 @@ +fixture = \Stripe\V2\Collection::constructFrom([ + 'data' => [ + ['id' => 'pm_123', 'object' => 'pageablemodel'], + ['id' => 'pm_456', 'object' => 'pageablemodel'], + ], + 'next_page_url' => '/v2/pageablemodel?page=page_2', + 'previous_page_url' => null, + ], ['api_key' => 'sk_test', 'stripe_context' => 'wksp_123'], 'v2'); + } + + public function testOffsetGetNumericIndex() + { + $this->expectException(\Stripe\Exception\InvalidArgumentException::class); + $this->compatExpectExceptionMessageMatches('/You tried to access the \\d index/'); + + $this->fixture[0]; + } + + public function testCanCount() + { + $collection = \Stripe\V2\Collection::constructFrom([ + 'data' => [['id' => '1']], + ]); + static::assertCount(1, $collection); + + $collection = \Stripe\V2\Collection::constructFrom([ + 'data' => [['id' => '1'], ['id' => '2'], ['id' => '3']], + ]); + static::assertCount(3, $collection); + } + + public function testCanIterate() + { + $collection = \Stripe\V2\Collection::constructFrom([ + 'data' => [['id' => '1'], ['id' => '2'], ['id' => '3']], + 'next_page_url' => null, + 'previous_page_url' => null, + ]); + + $seen = []; + foreach ($collection as $item) { + $seen[] = $item['id']; + } + + static::assertSame(['1', '2', '3'], $seen); + } + + public function testCanIterateBackwards() + { + $collection = \Stripe\V2\Collection::constructFrom([ + 'data' => [['id' => '1'], ['id' => '2'], ['id' => '3']], + 'next_page_url' => null, + 'previous_page_url' => null, + ]); + + $seen = []; + foreach ($collection->getReverseIterator() as $item) { + $seen[] = $item['id']; + } + + static::assertSame(['3', '2', '1'], $seen); + } + + public function testSupportsIteratorToArray() + { + $seen = []; + foreach (\iterator_to_array($this->fixture) as $item) { + $seen[] = $item['id']; + } + + static::assertSame(['pm_123', 'pm_456'], $seen); + } + + public function testAutoPagingIteratorSupportsOnePage() + { + $lo = \Stripe\V2\Collection::constructFrom([ + 'data' => [ + ['id' => '1'], + ['id' => '2'], + ['id' => '3'], + ], + 'next_page_url' => null, + 'previous_page_url' => null, + ]); + + $seen = []; + foreach ($lo->autoPagingIterator() as $item) { + $seen[] = $item['id']; + } + + static::assertSame(['1', '2', '3'], $seen); + } + + public function testAutoPagingIteratorSupportsTwoPages() + { + $lo = \Stripe\V2\Collection::constructFrom([ + 'data' => [ + ['id' => '1'], + ], + 'next_page_url' => '/v2/pageablemodel?foo=bar&page=page_2', + 'previous_page_url' => null, + ]); + + $this->stubRequest( + 'GET', + '/v2/pageablemodel?foo=bar&page=page_2', + null, + null, + false, + [ + 'data' => [ + ['id' => '2'], + ['id' => '3'], + ], + 'next_page_url' => null, + 'previous_page_url' => null, + ] + ); + + $seen = []; + foreach ($lo->autoPagingIterator() as $item) { + $seen[] = $item['id']; + } + + static::assertSame(['1', '2', '3'], $seen); + } + + public function testAutoPagingIteratorSupportsIteratorToArray() + { + $this->stubRequest( + 'GET', + '/v2/pageablemodel?page=page_2', + null, + null, + false, + [ + 'data' => [['id' => 'pm_789']], + 'next_page_url' => null, + 'previous_page_url' => null, + ] + ); + + $seen = []; + foreach (\iterator_to_array($this->fixture->autoPagingIterator()) as $item) { + $seen[] = $item['id']; + } + + static::assertSame(['pm_123', 'pm_456', 'pm_789'], $seen); + } + + public function testForwardsRequestOpts() + { + $curlClientStub = $this->getMockBuilder(\Stripe\HttpClient\CurlClient::class) + ->setMethods(['executeRequestWithRetries']) + ->getMock() + ; + + $curlClientStub->method('executeRequestWithRetries') + ->willReturnOnConsecutiveCalls([ + '{"data": [{"id": "pm_777"}], "next_page_url": "/v2/pageablemodel?page_3", "previous_page_url": "/v2/pageablemodel?page_1"}', + 200, + [], + ], [ + '{"data": [{"id": "pm_888"}], "next_page_url": null, "previous_page_url": "/v2/pageablemodel?page_2"}', + 200, + [], + ]) + ; + + $curlClientStub->expects(static::exactly(2)) + ->method('executeRequestWithRetries') + ->with(static::callback(function ($opts) { + $this->assertContains('Authorization: Bearer sk_test', $opts[\CURLOPT_HTTPHEADER]); + $this->assertContains('Stripe-Context: wksp_123', $opts[\CURLOPT_HTTPHEADER]); + + return true; + })) + ; + + \Stripe\ApiRequestor::setHttpClient($curlClientStub); + + $seen = []; + foreach ($this->fixture->autoPagingIterator() as $item) { + $seen[] = $item['id']; + } + + static::assertSame(['pm_123', 'pm_456', 'pm_777', 'pm_888'], $seen); + } +} diff --git a/tests/Stripe/WebhookTest.php b/tests/Stripe/WebhookTest.php index eeebd0324..bebb7e30a 100644 --- a/tests/Stripe/WebhookTest.php +++ b/tests/Stripe/WebhookTest.php @@ -18,7 +18,7 @@ final class WebhookTest extends \Stripe\TestCase }'; const SECRET = 'whsec_test_secret'; - private function generateHeader($opts = []) + public static function generateHeader($opts = []) { $timestamp = \array_key_exists('timestamp', $opts) ? $opts['timestamp'] : \time(); $payload = \array_key_exists('payload', $opts) ? $opts['payload'] : self::EVENT_PAYLOAD; diff --git a/tests/TestHelper.php b/tests/TestHelper.php index 06319c7c0..dee100da7 100644 --- a/tests/TestHelper.php +++ b/tests/TestHelper.php @@ -177,8 +177,9 @@ protected function stubRequest( * @param string $path relative path (e.g. '/v1/charges') * @param null|array $params array of parameters. If null, parameters will * not be checked. - * @param null|string[] $headers array of headers. Does not need to be - * exhaustive. If null, headers are not checked. + * @param null|callable|string[] $headers array of headers or a callback to implement + * custom logic. String array does not does not need to be exhaustive. If null, + * headers are not checked. * @param bool $hasFile Whether the request parameters contains a file. * Defaults to false. * @param null|string $base base URL (e.g. 'https://api.stripe.com') @@ -208,15 +209,17 @@ private function prepareRequestMock( static::identicalTo($absUrl), // for headers, we only check that all of the headers provided in $headers are // present in the list of headers of the actual request - null === $headers ? static::anything() : static::callback(function ($array) use ($headers) { - foreach ($headers as $header) { - if (!\in_array($header, $array, true)) { - return false; + null === $headers ? static::anything() : static::callback( + \is_callable($headers) ? $headers : function ($array) use ($headers) { + foreach ($headers as $header) { + if (!\in_array($header, $array, true)) { + return false; + } } - } - return true; - }), + return true; + } + ), null === $params ? static::anything() : static::identicalTo($params), static::identicalTo($hasFile) ) @@ -233,8 +236,9 @@ private function prepareRequestMock( * @param string $path relative path (e.g. '/v1/charges') * @param null|array $params array of parameters. If null, parameters will * not be checked. - * @param null|string[] $headers array of headers. Does not need to be - * exhaustive. If null, headers are not checked. + * @param null|callable|string[] $headers array of headers or a callback to implement + * custom logic. String array does not does not need to be exhaustive. If null, + * headers are not checked. * @param bool $hasFile Whether the request parameters contains a file. * Defaults to false. * @param null|string $base base URL (e.g. 'https://api.stripe.com') @@ -263,15 +267,17 @@ private function prepareRequestStreamMock( static::identicalTo($absUrl), // for headers, we only check that all of the headers provided in $headers are // present in the list of headers of the actual request - null === $headers ? static::anything() : static::callback(function ($array) use ($headers) { - foreach ($headers as $header) { - if (!\in_array($header, $array, true)) { - return false; + null === $headers ? static::anything() : static::callback( + \is_callable($headers) ? $headers : function ($array) use ($headers) { + foreach ($headers as $header) { + if (!\in_array($header, $array, true)) { + return false; + } } - } - return true; - }), + return true; + } + ), null === $params ? static::anything() : static::identicalTo($params), static::identicalTo($hasFile), static::anything()