From 002550f739206025c4cd3137089864f3bea1ef47 Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Wed, 25 Sep 2024 11:53:14 -0700 Subject: [PATCH 1/7] added new example template and instructions on how to create more --- examples/README.md | 11 +++++++ examples/meter_event_stream.php | 55 +++++++++++++++++++++++++++++++++ examples/new_example.php | 28 +++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/meter_event_stream.php create mode 100644 examples/new_example.php 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/meter_event_stream.php b/examples/meter_event_stream.php new file mode 100644 index 000000000..4ea399ac0 --- /dev/null +++ b/examples/meter_event_stream.php @@ -0,0 +1,55 @@ +apiKey = $apiKey; + $this->meterEventSession = null; + } + + private function refreshMeterEventSession() + { + // Check if session is null or expired + if ( + $this->meterEventSession === null || + $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 MeterEventManager($apiKey); +$manager->sendMeterEvent([ + 'event_name' => 'alpaca_ai_tokens', + 'payload' => [ + 'stripe_customer_id' => $customerId, + 'value' => '26', + ], +]); diff --git a/examples/new_example.php b/examples/new_example.php new file mode 100644 index 000000000..c2b6da52f --- /dev/null +++ b/examples/new_example.php @@ -0,0 +1,28 @@ +apiKey = $apiKey; + } + + public function doSomethingGreat() + { + print("Hello World\n"); + // $client = new \Stripe\StripeClient($this->apiKey); + } +} + +// Usage +$apiKey = "{{API_KEY}}"; + +$example = new NewExample($apiKey); +$example->doSomethingGreat(); From 3a88f9079bd048a0577ebd9d6024315e87fc4b27 Mon Sep 17 00:00:00 2001 From: Jesse Rosalia Date: Wed, 25 Sep 2024 11:59:22 -0700 Subject: [PATCH 2/7] Revert "added new example template and instructions on how to create more" This reverts commit 002550f739206025c4cd3137089864f3bea1ef47. --- examples/README.md | 11 ------- examples/meter_event_stream.php | 55 --------------------------------- examples/new_example.php | 28 ----------------- 3 files changed, 94 deletions(-) delete mode 100644 examples/README.md delete mode 100644 examples/meter_event_stream.php delete mode 100644 examples/new_example.php diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 8ba8046cb..000000000 --- a/examples/README.md +++ /dev/null @@ -1,11 +0,0 @@ -## 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/meter_event_stream.php b/examples/meter_event_stream.php deleted file mode 100644 index 4ea399ac0..000000000 --- a/examples/meter_event_stream.php +++ /dev/null @@ -1,55 +0,0 @@ -apiKey = $apiKey; - $this->meterEventSession = null; - } - - private function refreshMeterEventSession() - { - // Check if session is null or expired - if ( - $this->meterEventSession === null || - $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 MeterEventManager($apiKey); -$manager->sendMeterEvent([ - 'event_name' => 'alpaca_ai_tokens', - 'payload' => [ - 'stripe_customer_id' => $customerId, - 'value' => '26', - ], -]); diff --git a/examples/new_example.php b/examples/new_example.php deleted file mode 100644 index c2b6da52f..000000000 --- a/examples/new_example.php +++ /dev/null @@ -1,28 +0,0 @@ -apiKey = $apiKey; - } - - public function doSomethingGreat() - { - print("Hello World\n"); - // $client = new \Stripe\StripeClient($this->apiKey); - } -} - -// Usage -$apiKey = "{{API_KEY}}"; - -$example = new NewExample($apiKey); -$example->doSomethingGreat(); From b12bec99bb1dd2c67c51c1252c0e53bfb45eb9dd Mon Sep 17 00:00:00 2001 From: Ramya Rao <100975018+ramya-stripe@users.noreply.github.com> Date: Tue, 1 Oct 2024 09:33:07 -0700 Subject: [PATCH 3/7] Support for APIs in the new API version 2024-09-30.acacia (#1756) --- OPENAPI_VERSION | 2 +- README.md | 20 + examples/README.md | 11 + examples/meter_event_stream.php | 53 ++ examples/new_example.php | 26 + examples/stripe_webhook_handler.php | 34 ++ init.php | 31 +- lib/ApiOperations/Request.php | 10 +- lib/ApiRequestor.php | 108 +++- lib/BaseStripeClient.php | 192 ++++++- lib/BaseStripeClientInterface.php | 7 + lib/Billing/Alert.php | 3 +- lib/Billing/CreditBalanceSummary.php | 36 ++ lib/Billing/CreditBalanceTransaction.php | 63 +++ lib/Billing/CreditGrant.php | 149 ++++++ lib/Capability.php | 2 +- lib/CreditNote.php | 1 + lib/CreditNoteLineItem.php | 1 + lib/Customer.php | 5 +- lib/ErrorObject.php | 3 +- ...lingMeterErrorReportTriggeredEventData.php | 15 + .../V1BillingMeterNoMeterFoundEventData.php | 15 + ...1BillingMeterErrorReportTriggeredEvent.php | 35 ++ .../V1BillingMeterNoMeterFoundEvent.php | 13 + .../TemporarySessionExpiredException.php | 9 + lib/HttpClient/ClientInterface.php | 3 +- lib/HttpClient/CurlClient.php | 166 +++++-- lib/Invoice.php | 1 + lib/InvoiceLineItem.php | 1 + lib/Margin.php | 24 + lib/OAuthErrorObject.php | 3 +- lib/PromotionCode.php | 2 +- lib/Reason.php | 13 + lib/RelatedObject.php | 15 + lib/Service/AbstractServiceFactory.php | 45 +- lib/Service/Billing/BillingServiceFactory.php | 6 + .../Billing/CreditBalanceSummaryService.php | 27 + .../CreditBalanceTransactionService.php | 43 ++ lib/Service/Billing/CreditGrantService.php | 106 ++++ lib/Service/CoreServiceFactory.php | 2 + lib/Service/ServiceNavigatorTrait.php | 58 +++ lib/Service/SubscriptionService.php | 33 +- .../V2/Billing/BillingServiceFactory.php | 31 ++ .../Billing/MeterEventAdjustmentService.php | 27 + lib/Service/V2/Billing/MeterEventService.php | 29 ++ .../V2/Billing/MeterEventSessionService.php | 29 ++ .../V2/Billing/MeterEventStreamService.php | 33 ++ lib/Service/V2/Core/CoreServiceFactory.php | 27 + lib/Service/V2/Core/EventService.php | 43 ++ lib/Service/V2/V2ServiceFactory.php | 27 + lib/Stripe.php | 6 + lib/StripeClient.php | 1 + lib/StripeObject.php | 31 +- lib/Tax/Settings.php | 2 +- lib/ThinEvent.php | 23 + lib/Treasury/ReceivedCredit.php | 1 + lib/Util/ApiVersion.php | 2 +- lib/Util/EventTypes.php | 13 + lib/Util/ObjectTypes.php | 20 + lib/Util/RequestOptions.php | 24 +- lib/Util/Util.php | 142 +++++- lib/V2/Billing/MeterEvent.php | 21 + lib/V2/Billing/MeterEventAdjustment.php | 23 + lib/V2/Billing/MeterEventSession.php | 18 + lib/V2/Collection.php | 110 ++++ lib/V2/Event.php | 16 + lib/Webhook.php | 4 +- tests/Stripe/ApiRequestorTest.php | 111 +++++ tests/Stripe/BaseStripeClientTest.php | 469 ++++++++++++++++++ tests/Stripe/GeneratedExamplesTest.php | 16 - tests/Stripe/HttpClient/CurlClientTest.php | 92 +++- tests/Stripe/StripeClientTest.php | 73 ++- tests/Stripe/Util/RequestOptionsTest.php | 1 + tests/Stripe/Util/UtilTest.php | 63 +++ tests/Stripe/V2/CollectionTest.php | 209 ++++++++ tests/Stripe/WebhookTest.php | 2 +- tests/TestHelper.php | 42 +- 77 files changed, 2919 insertions(+), 254 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/meter_event_stream.php create mode 100644 examples/new_example.php create mode 100644 examples/stripe_webhook_handler.php create mode 100644 lib/Billing/CreditBalanceSummary.php create mode 100644 lib/Billing/CreditBalanceTransaction.php create mode 100644 lib/Billing/CreditGrant.php create mode 100644 lib/EventData/V1BillingMeterErrorReportTriggeredEventData.php create mode 100644 lib/EventData/V1BillingMeterNoMeterFoundEventData.php create mode 100644 lib/Events/V1BillingMeterErrorReportTriggeredEvent.php create mode 100644 lib/Events/V1BillingMeterNoMeterFoundEvent.php create mode 100644 lib/Exception/TemporarySessionExpiredException.php create mode 100644 lib/Margin.php create mode 100644 lib/Reason.php create mode 100644 lib/RelatedObject.php create mode 100644 lib/Service/Billing/CreditBalanceSummaryService.php create mode 100644 lib/Service/Billing/CreditBalanceTransactionService.php create mode 100644 lib/Service/Billing/CreditGrantService.php create mode 100644 lib/Service/ServiceNavigatorTrait.php create mode 100644 lib/Service/V2/Billing/BillingServiceFactory.php create mode 100644 lib/Service/V2/Billing/MeterEventAdjustmentService.php create mode 100644 lib/Service/V2/Billing/MeterEventService.php create mode 100644 lib/Service/V2/Billing/MeterEventSessionService.php create mode 100644 lib/Service/V2/Billing/MeterEventStreamService.php create mode 100644 lib/Service/V2/Core/CoreServiceFactory.php create mode 100644 lib/Service/V2/Core/EventService.php create mode 100644 lib/Service/V2/V2ServiceFactory.php create mode 100644 lib/ThinEvent.php create mode 100644 lib/Util/EventTypes.php create mode 100644 lib/V2/Billing/MeterEvent.php create mode 100644 lib/V2/Billing/MeterEventAdjustment.php create mode 100644 lib/V2/Billing/MeterEventSession.php create mode 100644 lib/V2/Collection.php create mode 100644 lib/V2/Event.php create mode 100644 tests/Stripe/V2/CollectionTest.php 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/README.md b/README.md index 436aef2cb..b51c486dd 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,26 @@ If your beta feature requires a `Stripe-Version` header to be sent, set the `api Stripe::addBetaVersion("feature_beta", "v3"); ``` +### Custom requests + +If you would like to send a request to an undocumented API (for example you are in a private beta), or if you prefer to bypass the method definitions in the library and specify your request details directly, you can use the `rawRequest` method on the StripeClient. + +```php +$stripe = new \Stripe\StripeClient('sk_test_xyz'); +$response = $stripe->rawRequest('post', '/v1/beta_endpoint', [ + "caveat": "emptor" +], [ + "stripe_version" => "2022-11_15", +]); +// $response->body is a string, you can call $stripe->deserialize to get a \Stripe\StripeObject. +$obj = $stripe->deserialize($response->body); + +// For GET requests, the params argument must be null, and you should write the query string explicitly. +$get_response = $stripe->rawRequest('get', '/v1/beta_endpoint?caveat=emptor', null, [ + "stripe_version" => "2022-11_15", +]); +``` + ## Support New features and bug fixes are released on the latest major version of the Stripe PHP library. If you are on an older major version, we recommend that you upgrade to the latest in order to use the new features and bug fixes including those for security vulnerabilities. Older major versions of the package will continue to be available for use, but will not be receiving any updates. 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/meter_event_stream.php b/examples/meter_event_stream.php new file mode 100644 index 000000000..9c520dfa3 --- /dev/null +++ b/examples/meter_event_stream.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 MeterEventManager($apiKey); +$manager->sendMeterEvent([ + 'event_name' => 'alpaca_ai_tokens', + 'payload' => [ + 'stripe_customer_id' => $customerId, + 'value' => '26', + ], +]); diff --git a/examples/new_example.php b/examples/new_example.php new file mode 100644 index 000000000..bf5c4d7a2 --- /dev/null +++ b/examples/new_example.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/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 63fcfe467..8a5da73db 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,10 +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/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'; @@ -94,6 +100,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/MeterEvent.php'; require __DIR__ . '/lib/Billing/MeterEventAdjustment.php'; @@ -125,6 +134,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'; @@ -152,6 +166,7 @@ require __DIR__ . '/lib/LineItem.php'; require __DIR__ . '/lib/LoginLink.php'; require __DIR__ . '/lib/Mandate.php'; +require __DIR__ . '/lib/Margin.php'; require __DIR__ . '/lib/PaymentIntent.php'; require __DIR__ . '/lib/PaymentLink.php'; require __DIR__ . '/lib/PaymentMethod.php'; @@ -184,6 +199,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'; @@ -309,6 +327,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'; @@ -352,6 +378,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 8165a08d2..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, $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 7ba35661a..0476346c0 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. @@ -116,23 +116,24 @@ private static function _encodeObjects($d) } /** - * @param 'delete'|'get'|'post' $method + * @param 'delete'|'get'|'post' $method * @param string $url * @param null|array $params * @param null|array $headers + * @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, $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, $usage); - $json = $this->_interpretResponse($rbody, $rcode, $rheaders); + $this->_requestRaw($method, $url, $params, $headers, $apiMode, $usage); + $json = $this->_interpretResponse($rbody, $rcode, $rheaders, $apiMode); $resp = new ApiResponse($rbody, $rcode, $rheaders, $json); return [$resp, $myApiKey]; @@ -144,18 +145,19 @@ public function request($method, $url, $params = null, $headers = null, $usage = * @param callable $readBodyChunkCallable * @param null|array $params * @param null|array $headers + * @param 'v1'|'v2' $apiMode * @param string[] $usage * * @throws Exception\ApiErrorException */ - public function requestStream($method, $url, $readBodyChunkCallable, $params = null, $headers = null, $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, $usage, $readBodyChunkCallable); + $this->_requestRawStreaming($method, $url, $params, $headers, $apiMode, $usage, $readBodyChunkCallable); if ($rcode >= 300) { - $this->_interpretResponse($rbody, $rcode, $rheaders); + $this->_interpretResponse($rbody, $rcode, $rheaders, $apiMode); } } @@ -164,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} " @@ -180,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; @@ -201,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'] : null; $param = isset($errorData['param']) ? $errorData['param'] : null; @@ -220,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); @@ -241,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 * @@ -330,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'); @@ -366,7 +409,14 @@ private static function _defaultHeaders($apiKey, $clientInfo = null, $appInfo = ]; } - private function _prepareRequest($method, $url, $params, $headers) + /** + * @param 'delete'|'get'|'post' $method + * @param string $url + * @param array $params + * @param array $headers + * @param 'v1'|'v2' $apiMode + */ + private function _prepareRequest($method, $url, $params, $headers, $apiMode) { $myApiKey = $this->_apiKey; if (!$myApiKey) { @@ -406,8 +456,10 @@ function ($key) use ($params) { } $absUrl = $this->_apiBase . $url; - $params = self::_encodeObjects($params); - $defaultHeaders = $this->_defaultHeaders($myApiKey, $clientUAInfo, $this->_appInfo); + if ('v1' === $apiMode) { + $params = self::_encodeObjects($params); + } + $defaultHeaders = $this->_defaultHeaders($myApiKey, $clientUAInfo, $this->_appInfo, $apiMode); if (Stripe::$accountId) { $defaultHeaders['Stripe-Account'] = Stripe::$accountId; @@ -429,8 +481,12 @@ function ($key) use ($params) { if ($hasFile) { $defaultHeaders['Content-Type'] = 'multipart/form-data'; - } else { + } elseif ('v2' === $apiMode) { + $defaultHeaders['Content-Type'] = 'application/json'; + } elseif ('v1' === $apiMode) { $defaultHeaders['Content-Type'] = 'application/x-www-form-urlencoded'; + } else { + throw new Exception\InvalidArgumentException('Unknown API mode: ' . $apiMode); } $combinedHeaders = \array_merge($defaultHeaders, $headers); @@ -448,6 +504,7 @@ function ($key) use ($params) { * @param string $url * @param array $params * @param array $headers + * @param 'v1'|'v2' $apiMode * @param string[] $usage * * @throws Exception\AuthenticationException @@ -455,9 +512,9 @@ function ($key) use ($params) { * * @return array */ - private function _requestRaw($method, $url, $params, $headers, $usage) + private function _requestRaw($method, $url, $params, $headers, $apiMode, $usage) { - list($absUrl, $rawHeaders, $params, $hasFile, $myApiKey) = $this->_prepareRequest($method, $url, $params, $headers); + list($absUrl, $rawHeaders, $params, $hasFile, $myApiKey) = $this->_prepareRequest($method, $url, $params, $headers, $apiMode); $requestStartMs = Util\Util::currentTimeMillis(); @@ -466,7 +523,8 @@ private function _requestRaw($method, $url, $params, $headers, $usage) $absUrl, $rawHeaders, $params, - $hasFile + $hasFile, + $apiMode ); if ( @@ -491,15 +549,16 @@ private function _requestRaw($method, $url, $params, $headers, $usage) * @param array $headers * @param string[] $usage * @param callable $readBodyChunkCallable + * @param 'v1'|'v2' $apiMode * * @throws Exception\AuthenticationException * @throws Exception\ApiConnectionException * * @return array */ - private function _requestRawStreaming($method, $url, $params, $headers, $usage, $readBodyChunkCallable) + private function _requestRawStreaming($method, $url, $params, $headers, $apiMode, $usage, $readBodyChunkCallable) { - list($absUrl, $rawHeaders, $params, $hasFile, $myApiKey) = $this->_prepareRequest($method, $url, $params, $headers); + list($absUrl, $rawHeaders, $params, $hasFile, $myApiKey) = $this->_prepareRequest($method, $url, $params, $headers, $apiMode); $requestStartMs = Util\Util::currentTimeMillis(); @@ -556,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(); @@ -574,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 38651bf2c..71faa4021 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 */ @@ -45,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. * @@ -57,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 @@ -76,6 +87,7 @@ 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'], ]); } @@ -130,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. * @@ -152,17 +174,67 @@ 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, ['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; } + /** + * Sends a raw request to Stripe's API. This is the lowest level method for interacting + * with the Stripe API. This method is useful for interacting with endpoints that are not + * covered yet in stripe-php. + * + * @param 'delete'|'get'|'post' $method the HTTP method + * @param string $path the path of the request + * @param null|array $params the parameters of the request + * @param array $opts the special modifiers of the request + * + * @return \Stripe\ApiResponse + */ + 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 = \Stripe\Util\Util::getApiMode($path); + $headers = []; + if (\is_array($opts) && \array_key_exists('headers', $opts)) { + $headers = $opts['headers'] ?: []; + unset($opts['headers']); + } + if (\is_array($opts) && \array_key_exists('stripe_context', $opts)) { + $headers['Stripe-Context'] = $opts['stripe_context']; + unset($opts['stripe_context']); + } + + $defaultRawRequestOpts = $this->defaultOpts; + + $opts = $defaultRawRequestOpts->merge($opts, true); + + // Concatenate $headers to $opts->headers, removing duplicates. + $opts->headers = \array_merge($opts->headers, $headers); + $baseUrl = $opts->apiBase ?: $this->getApiBase(); + $requestor = new \Stripe\ApiRequestor($this->apiKeyForRequest($opts), $baseUrl); + list($response) = $requestor->request($method, $path, $params, $opts->headers, $apiMode, ['raw_request']); + + return $response; + } + /** * Sends a request to Stripe's API, passing chunks of the streamed response * into a user-provided $readBodyChunkCallable callback. @@ -172,6 +244,7 @@ public function request($method, $path, $params, $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) @@ -179,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, ['stripe_client']); + $apiMode = \Stripe\Util\Util::getApiMode($path); + list($response, $opts->apiKey) = $requestor->requestStream($method, $path, $readBodyChunkCallable, $params, $opts->headers, $apiMode, ['stripe_client']); } /** @@ -190,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; } @@ -286,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'); @@ -327,4 +416,85 @@ private function validateConfig($config) throw new \Stripe\Exception\InvalidArgumentException('Found unknown key(s) in configuration array: ' . $invalidKeys); } } + + /** + * 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, $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) + { + $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(); + } + } + + /** + * Returns an 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 + * + * @throws Exception\UnexpectedValueException if the payload is not valid JSON, + * @throws Exception\SignatureVerificationException if the verification fails + * + * @return Event the Event instance + */ + public function parseSnapshotEvent($payload, $sigHeader, $secret, $tolerance = Webhook::DEFAULT_TOLERANCE) + { + WebhookSignature::verifyHeader($payload, $sigHeader, $secret, $tolerance); + + $data = \json_decode($payload, true); + $jsonError = \json_last_error(); + if (null === $data && \JSON_ERROR_NONE !== $jsonError) { + $msg = "Invalid payload: {$payload} " + . "(json_last_error() was {$jsonError})"; + + throw new Exception\UnexpectedValueException($msg); + } + + return Event::constructFrom($data); + } } 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/CreditNote.php b/lib/CreditNote.php index 7173d10a4..475f3208c 100644 --- a/lib/CreditNote.php +++ b/lib/CreditNote.php @@ -28,6 +28,7 @@ * @property string $number A unique number that identifies this particular credit note and appears on the PDF of the credit note and its associated invoice. * @property null|int $out_of_band_amount Amount that was credited outside of Stripe. * @property string $pdf The link to download the PDF of the credit note. + * @property null|\Stripe\StripeObject[] $pretax_credit_amounts * @property null|string $reason Reason for issuing this credit note, one of duplicate, fraudulent, order_change, or product_unsatisfactory * @property null|string|\Stripe\Refund $refund Refund related to this credit note. * @property null|\Stripe\StripeObject $shipping_cost The details of the cost of shipping, including the ShippingRate applied to the invoice. 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 a7268d39c..02b5de3ef 100644 --- a/lib/ErrorObject.php +++ b/lib/ErrorObject.php @@ -223,8 +223,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 @@ +defaultOptions)) { // call defaultOptions callback, set options to return value - $opts = \call_user_func_array($this->defaultOptions, \func_get_args()); - if (!\is_array($opts)) { + $ret = \call_user_func_array($this->defaultOptions, [$method, $absUrl, $headers, $params, $hasFile]); + if (!\is_array($ret)) { throw new Exception\UnexpectedValueException('Non-array value returned by defaultOptions CurlClient callback'); } - } elseif (\is_array($this->defaultOptions)) { // set default curlopts from array - $opts = $this->defaultOptions; + + return $ret; + } + if (\is_array($this->defaultOptions)) { // set default curlopts from array + return $this->defaultOptions; } - $params = Util\Util::objectsToIds($params); + return []; + } + private function constructCurlOptions($method, $absUrl, $headers, $body, $opts, $apiMode) + { if ('get' === $method) { - if ($hasFile) { - throw new Exception\UnexpectedValueException( - 'Issuing a GET request with a file parameter' - ); - } $opts[\CURLOPT_HTTPGET] = 1; - if (\count($params) > 0) { - $encoded = Util\Util::encodeParameters($params); - $absUrl = "{$absUrl}?{$encoded}"; - } } elseif ('post' === $method) { $opts[\CURLOPT_POST] = 1; - $opts[\CURLOPT_POSTFIELDS] = $hasFile ? $params : Util\Util::encodeParameters($params); } elseif ('delete' === $method) { $opts[\CURLOPT_CUSTOMREQUEST] = 'DELETE'; - if (\count($params) > 0) { - $encoded = Util\Util::encodeParameters($params); - $absUrl = "{$absUrl}?{$encoded}"; - } } else { throw new Exception\UnexpectedValueException("Unrecognized method {$method}"); } - // It is only safe to retry network failures on POST requests if we - // add an Idempotency-Key header - if (('post' === $method) && (Stripe::$maxNetworkRetries > 0)) { - if (!$this->hasHeader($headers, 'Idempotency-Key')) { - $headers[] = 'Idempotency-Key: ' . $this->randomGenerator->uuid(); + if ($body) { + $opts[\CURLOPT_POSTFIELDS] = $body; + } + // 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(); + } } } @@ -255,7 +296,6 @@ private function constructRequest($method, $absUrl, $headers, $params, $hasFile) // sending an empty `Expect:` header. $headers[] = 'Expect: '; - $absUrl = Util\Util::utf8($absUrl); $opts[\CURLOPT_URL] = $absUrl; $opts[\CURLOPT_RETURNTRANSFER] = true; $opts[\CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout; @@ -271,22 +311,56 @@ private function constructRequest($method, $absUrl, $headers, $params, $hasFile) $opts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2TLS; } - return [$opts, $absUrl]; + return $opts; } - public function request($method, $absUrl, $headers, $params, $hasFile) + /** + * @param 'delete'|'get'|'post' $method + * @param string $absUrl + * @param array $headers + * @param array $params + * @param bool $hasFile + * @param 'v1'|'v2' $apiMode + */ + private function constructRequest($method, $absUrl, $headers, $params, $hasFile, $apiMode) { - list($opts, $absUrl) = $this->constructRequest($method, $absUrl, $headers, $params, $hasFile); + $method = \strtolower($method); + + $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, $apiMode); + return [$opts, $absUrl]; + } + + /** + * @param 'delete'|'get'|'post' $method + * @param string $absUrl + * @param array $headers + * @param array $params + * @param bool $hasFile + * @param 'v1'|'v2' $apiMode + */ + 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); return [$rbody, $rcode, $rheaders]; } - public function requestStream($method, $absUrl, $headers, $params, $hasFile, $readBodyChunk) + /** + * @param 'delete'|'get'|'post' $method + * @param string $absUrl + * @param array $headers + * @param array $params + * @param bool $hasFile + * @param callable $readBodyChunk + * @param 'v1'|'v2' $apiMode + */ + public function requestStream($method, $absUrl, $headers, $params, $hasFile, $readBodyChunk, $apiMode = 'v1') { - list($opts, $absUrl) = $this->constructRequest($method, $absUrl, $headers, $params, $hasFile); - + list($opts, $absUrl) = $this->constructRequest($method, $absUrl, $headers, $params, $hasFile, $apiMode); $opts[\CURLOPT_RETURNTRANSFER] = false; list($rbody, $rcode, $rheaders) = $this->executeStreamingRequestWithRetries($opts, $absUrl, $readBodyChunk); @@ -378,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); @@ -540,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/Invoice.php b/lib/Invoice.php index e15ada76d..ef1db5d06 100644 --- a/lib/Invoice.php +++ b/lib/Invoice.php @@ -118,6 +118,7 @@ * @property int $total Total after discounts and taxes. * @property null|\Stripe\StripeObject[] $total_discount_amounts The aggregate amounts calculated per discount across all line items. * @property null|int $total_excluding_tax The integer amount in cents (or local equivalent) representing the total amount of the invoice including all discounts but excluding all tax. + * @property null|\Stripe\StripeObject[] $total_pretax_credit_amounts * @property \Stripe\StripeObject[] $total_tax_amounts The aggregate amounts calculated per tax rate for all line items. * @property null|\Stripe\StripeObject $transfer_data The account (if any) the payment will be attributed to for tax reporting, and where funds from the payment will be transferred to for the invoice. * @property null|int $webhooks_delivered_at Invoices are automatically paid or sent 1 hour after webhooks are delivered, or until all webhook delivery attempts have been exhausted. This field tracks the time when webhooks for this invoice were successfully delivered. If the invoice had no webhooks to deliver, this will be set while the invoice is being created. diff --git a/lib/InvoiceLineItem.php b/lib/InvoiceLineItem.php index 2344d115a..6de28c98b 100644 --- a/lib/InvoiceLineItem.php +++ b/lib/InvoiceLineItem.php @@ -24,6 +24,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/Margin.php b/lib/Margin.php new file mode 100644 index 000000000..9036f8150 --- /dev/null +++ b/lib/Margin.php @@ -0,0 +1,24 @@ +true. + * @property int $created Time at which the object was created. Measured in seconds since the Unix epoch. + * @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 $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 Name of the margin that's displayed on, for example, invoices. + * @property float $percent_off Percent that will be taken off the subtotal before tax (after all other discounts and promotions) of any invoice to which the margin is applied. + * @property int $updated Time at which the object was last updated. Measured in seconds since the Unix epoch. + */ +class Margin extends ApiResource +{ + const OBJECT_NAME = 'margin'; +} 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/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 6c9af6d1b..150420417 100644 --- a/lib/Service/CoreServiceFactory.php +++ b/lib/Service/CoreServiceFactory.php @@ -74,6 +74,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 */ @@ -152,6 +153,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 cfbee32fb..1333b1869 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 d46751f14..f8c07d4d4 100644 --- a/lib/StripeClient.php +++ b/lib/StripeClient.php @@ -74,6 +74,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..ce27fac51 --- /dev/null +++ b/lib/ThinEvent.php @@ -0,0 +1,23 @@ +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. + */ +class ThinEvent +{ + public $id; + public $type; + public $created; + public $context; + public $related_object; +} diff --git a/lib/Treasury/ReceivedCredit.php b/lib/Treasury/ReceivedCredit.php index 5694e4365..a2a1cd17d 100644 --- a/lib/Treasury/ReceivedCredit.php +++ b/lib/Treasury/ReceivedCredit.php @@ -30,6 +30,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/ApiVersion.php b/lib/Util/ApiVersion.php index ccf8dc380..09aca0a62 100644 --- a/lib/Util/ApiVersion.php +++ b/lib/Util/ApiVersion.php @@ -6,5 +6,5 @@ class ApiVersion { - const CURRENT = '2024-06-20'; + const CURRENT = '2024-09-30.acacia'; } 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 cbe5a4f70..f753b3f14 100644 --- a/lib/Util/ObjectTypes.php +++ b/lib/Util/ObjectTypes.php @@ -27,6 +27,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\MeterEvent::OBJECT_NAME => \Stripe\Billing\MeterEvent::class, \Stripe\Billing\MeterEventAdjustment::OBJECT_NAME => \Stripe\Billing\MeterEventAdjustment::class, @@ -85,6 +88,7 @@ class ObjectTypes \Stripe\LineItem::OBJECT_NAME => \Stripe\LineItem::class, \Stripe\LoginLink::OBJECT_NAME => \Stripe\LoginLink::class, \Stripe\Mandate::OBJECT_NAME => \Stripe\Mandate::class, + \Stripe\Margin::OBJECT_NAME => \Stripe\Margin::class, \Stripe\PaymentIntent::OBJECT_NAME => \Stripe\PaymentIntent::class, \Stripe\PaymentLink::OBJECT_NAME => \Stripe\PaymentLink::class, \Stripe\PaymentMethod::OBJECT_NAME => \Stripe\PaymentMethod::class, @@ -151,4 +155,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..eb574aeaa 100644 --- a/lib/Util/Util.php +++ b/lib/Util/Util.php @@ -36,35 +36,81 @@ 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); + } else { + $property->setAccessible(true); + $property->setValue($instance, $json[$property->getName()]); + } + } + } + + return $instance; + } + /** * @param mixed|string $value a string to UTF8-encode * @@ -74,17 +120,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 +214,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 +231,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 +265,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 +344,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( + 'GET', + '/v2/core/events/evt_123', + [], + null, + false, + [ + 'error' => [ + 'type' => 'temporary_session_expired', + 'code' => 'session_bad', + 'message' => 'you messed up', + ], + ], + 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 (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('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)); + } + } + public function testHeaderStripeVersionGlobal() { Stripe::setApiVersion('2222-22-22'); @@ -582,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 4a002c63c..87ece8304 100644 --- a/tests/Stripe/BaseStripeClientTest.php +++ b/tests/Stripe/BaseStripeClientTest.php @@ -2,6 +2,8 @@ namespace Stripe; +use Stripe\Util\ApiVersion; + /** * @internal * @covers \Stripe\BaseStripeClient @@ -9,9 +11,20 @@ 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; + } + /** @before */ protected function setUpOptsReflector() { @@ -19,6 +32,21 @@ protected function setUpOptsReflector() $this->optsReflector->setAccessible(true); } + /** @before */ + 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(); @@ -221,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); @@ -389,4 +719,143 @@ public function testConfigValidationFindsExtraAppInfoKeys() ], ]); } + + public function testParseThinEvent() + { + $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', + ], + ]; + + $eventData = json_encode($jsonEvent); + $client = new BaseStripeClient(['api_key' => 'sk_test_client', 'api_base' => MOCK_URL, 'stripe_account' => 'acc_123']); + + $sigHeader = WebhookTest::generateHeader(['payload' => $eventData]); + $event = $client->parseThinEvent($eventData, $sigHeader, WebhookTest::SECRET); + + 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 testV2OverridesPreviewVersionIfPassedInRawRequestOptions() + { + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{"object": "account"}', 200, []]) + ; + + $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 . '/v2/accounts/acct_123') + ; + + ApiRequestor::setHttpClient($this->curlClientStub); + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'api_base' => MOCK_URL, + ]); + $params = []; + $client->rawRequest('post', '/v2/accounts/acct_123', $params, [ + 'stripe_version' => '2222-22-22.preview-v2', + ]); + } + + public function testV2OverridesPreviewVersionIfPassedInRequestOptions() + { + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{"object": "billing.meter_event_session"}', 200, []]) + ; + + $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 . '/v2/billing/meter_event_session/bmes_123') + ; + + ApiRequestor::setHttpClient($this->curlClientStub); + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'api_base' => MOCK_URL, + ]); + $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 testV1AndV2Request() + { + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturnOnConsecutiveCalls(['{"object": "billing.meter_event_session"}', 200, []], ['{"object": "billing.meter_event"}', 200, []]) + ; + + $this->curlClientStub + ->method('executeRequestWithRetries') + ->withConsecutive([static::callback(function ($opts) { + $this->assertContains('Stripe-Version: ' . ApiVersion::CURRENT, $opts[\CURLOPT_HTTPHEADER]); + + return true; + }), 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($this->curlClientStub); + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'api_base' => MOCK_URL, + ]); + $meterEventSession = $client->request('get', '/v2/billing/meter_event_session/bmes_123', [], []); + static::assertNotNull($meterEventSession); + static::assertInstanceOf(\Stripe\V2\Billing\MeterEventSession::class, $meterEventSession); + + $meterEvent = $client->request('get', '/v1/billing/meter_event/bmes_123', [], []); + static::assertNotNull($meterEvent); + static::assertInstanceOf(\Stripe\Billing\MeterEvent::class, $meterEvent); + } + + public function testV2RequestWithEmptyResponse() + { + $this->curlClientStub->method('executeRequestWithRetries') + ->willReturn(['{}', 200, []]) + ; + + $this->curlClientStub->expects(static::once()) + ->method('executeRequestWithRetries') + ->with(static::callback(function ($opts) { + return true; + }), MOCK_URL . '/v2/billing/meter_event_stream') + ; + + ApiRequestor::setHttpClient($this->curlClientStub); + $client = new BaseStripeClient([ + 'api_key' => 'sk_test_client', + 'stripe_version' => '2222-22-22.preview-v2', + 'api_base' => MOCK_URL, + ]); + + $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 e7c8eabcc..1392fbc29 100644 --- a/tests/Stripe/GeneratedExamplesTest.php +++ b/tests/Stripe/GeneratedExamplesTest.php @@ -3581,22 +3581,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/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..0f0b15197 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,45 @@ 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', + ], + ]); + + $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); + } + + 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); + } } 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() From 3a9c9ac88628ca758da4a8efe8eaa29bdf6569da Mon Sep 17 00:00:00 2001 From: prathmesh-stripe <165320323+prathmesh-stripe@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:56:33 -0400 Subject: [PATCH 4/7] Removed parseSnapshotEvent (#1764) --- lib/BaseStripeClient.php | 34 ---------------------------------- lib/ThinEvent.php | 4 ++++ lib/Util/Util.php | 5 +++++ tests/Stripe/Util/UtilTest.php | 23 +++++++++++++++++++++++ 4 files changed, 32 insertions(+), 34 deletions(-) diff --git a/lib/BaseStripeClient.php b/lib/BaseStripeClient.php index 71faa4021..a2eb04a7b 100644 --- a/lib/BaseStripeClient.php +++ b/lib/BaseStripeClient.php @@ -463,38 +463,4 @@ public function parseThinEvent($payload, $sigHeader, $secret, $tolerance = Webho return new \Stripe\ThinEvent(); } } - - /** - * Returns an 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 - * - * @throws Exception\UnexpectedValueException if the payload is not valid JSON, - * @throws Exception\SignatureVerificationException if the verification fails - * - * @return Event the Event instance - */ - public function parseSnapshotEvent($payload, $sigHeader, $secret, $tolerance = Webhook::DEFAULT_TOLERANCE) - { - WebhookSignature::verifyHeader($payload, $sigHeader, $secret, $tolerance); - - $data = \json_decode($payload, true); - $jsonError = \json_last_error(); - if (null === $data && \JSON_ERROR_NONE !== $jsonError) { - $msg = "Invalid payload: {$payload} " - . "(json_last_error() was {$jsonError})"; - - throw new Exception\UnexpectedValueException($msg); - } - - return Event::constructFrom($data); - } } diff --git a/lib/ThinEvent.php b/lib/ThinEvent.php index ce27fac51..0ff3a2374 100644 --- a/lib/ThinEvent.php +++ b/lib/ThinEvent.php @@ -12,6 +12,8 @@ * @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 { @@ -20,4 +22,6 @@ class ThinEvent public $created; public $context; public $related_object; + public $reason; + public $livemode; } diff --git a/lib/Util/Util.php b/lib/Util/Util.php index eb574aeaa..f0ba9bb8f 100644 --- a/lib/Util/Util.php +++ b/lib/Util/Util.php @@ -101,6 +101,11 @@ public static function json_decode_thin_event_object($json, $class) $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()]); diff --git a/tests/Stripe/Util/UtilTest.php b/tests/Stripe/Util/UtilTest.php index 0f0b15197..aaf43d168 100644 --- a/tests/Stripe/Util/UtilTest.php +++ b/tests/Stripe/Util/UtilTest.php @@ -172,6 +172,10 @@ public function testJsonDecodeThinEventObject() '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); @@ -182,6 +186,8 @@ public function testJsonDecodeThinEventObject() 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() @@ -200,4 +206,21 @@ public function testJsonDecodeThinEventObjectWithNoRelatedObject() 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); + } } From 7a0b0283d492b66474ef9aa45a7acc938662b580 Mon Sep 17 00:00:00 2001 From: Ramya Rao Date: Tue, 1 Oct 2024 11:34:44 -0700 Subject: [PATCH 5/7] Bump version to 16.0.0 --- CHANGELOG.md | 23 +++++++++++++++++++++-- VERSION | 2 +- lib/Stripe.php | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de5d9f13e..14d34d4c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # 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.10.0 - 2024-09-18 * [#1747](https://github.com/stripe/stripe-php/pull/1747) Update generated code * Add support for new value `international_transaction` on enum `Treasury.ReceivedDebit.failure_code` @@ -14,8 +33,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.7.0 - 2024-08-15 diff --git a/VERSION b/VERSION index 9f7cbe79b..946789e61 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -15.10.0 +16.0.0 diff --git a/lib/Stripe.php b/lib/Stripe.php index 1333b1869..0110ab911 100644 --- a/lib/Stripe.php +++ b/lib/Stripe.php @@ -64,7 +64,7 @@ class Stripe /** @var float Initial delay between retries, in seconds */ private static $initialNetworkRetryDelay = 0.5; - const VERSION = '15.10.0'; + const VERSION = '16.0.0'; /** * @return string the API key used for requests From f57b7d884df5862d825c36a2ea2b2eb8afeb2bae Mon Sep 17 00:00:00 2001 From: Prathmesh Ranaut Date: Thu, 3 Oct 2024 14:43:18 -0400 Subject: [PATCH 6/7] Updated the class names --- examples/meter_event_stream.php | 2 +- examples/new_example.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/meter_event_stream.php b/examples/meter_event_stream.php index 9c520dfa3..8c3049734 100644 --- a/examples/meter_event_stream.php +++ b/examples/meter_event_stream.php @@ -2,7 +2,7 @@ require 'vendor/autoload.php'; // Make sure to include Composer's autoload file -class meter_event_stream +class MeterEventManager { private $apiKey; private $meterEventSession; diff --git a/examples/new_example.php b/examples/new_example.php index bf5c4d7a2..bde7007dc 100644 --- a/examples/new_example.php +++ b/examples/new_example.php @@ -1,9 +1,9 @@ Date: Thu, 3 Oct 2024 15:04:32 -0400 Subject: [PATCH 7/7] Fixed PHPcsfixer --- examples/{meter_event_stream.php => MeterEventStream.php} | 4 ++-- examples/{new_example.php => NewExample.php} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename examples/{meter_event_stream.php => MeterEventStream.php} (95%) rename examples/{new_example.php => NewExample.php} (100%) diff --git a/examples/meter_event_stream.php b/examples/MeterEventStream.php similarity index 95% rename from examples/meter_event_stream.php rename to examples/MeterEventStream.php index 8c3049734..cce30a0d6 100644 --- a/examples/meter_event_stream.php +++ b/examples/MeterEventStream.php @@ -2,7 +2,7 @@ require 'vendor/autoload.php'; // Make sure to include Composer's autoload file -class MeterEventManager +class MeterEventStream { private $apiKey; private $meterEventSession; @@ -43,7 +43,7 @@ public function sendMeterEvent($meterEvent) $apiKey = '{{API_KEY}}'; $customerId = '{{CUSTOMER_ID}}'; -$manager = new MeterEventManager($apiKey); +$manager = new MeterEventStream($apiKey); $manager->sendMeterEvent([ 'event_name' => 'alpaca_ai_tokens', 'payload' => [ diff --git a/examples/new_example.php b/examples/NewExample.php similarity index 100% rename from examples/new_example.php rename to examples/NewExample.php