diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e8aee24..7e981b8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## Next Release + +- Removes the deprecated, unusable `addCreditCard` function + - Stripe has disabled the ability to pass plain credit card details over the wire and now requires using [Stripe.js/Elements/Checkout](https://support.stripe.com/questions/card-tokenization-restrictions-using-publishable-keys). Follow the [Decentralized (EasyPost-Manage Billing) Guide](https://docs.easypost.com/guides/get-started-with-forge/easypost-managed-billing-guide#referralcustomer-billing-management) for more details on the new flow to use. + - Makes `referralCustomer.retrieveEasypostStripeApiKey` public to help facilitate adding credit cards using Stripe.js + ## v8.8.3 (2026-08-26) - Preserves caller-provided plain PHP objects in request params so `(object) []` is sent as an empty JSON object (`{}`) instead of being stringified diff --git a/lib/EasyPost/Constant/Constants.php b/lib/EasyPost/Constant/Constants.php index ec7b1023..522c2557 100644 --- a/lib/EasyPost/Constant/Constants.php +++ b/lib/EasyPost/Constant/Constants.php @@ -43,7 +43,6 @@ abstract class Constants const NO_USER_FOUND_ERROR = 'No user found with the given ID.'; const NO_RESPONSE_ERROR = 'Did not receive a response from %s.'; - const SEND_STRIPE_DETAILS_ERROR = 'Could not send card details to Stripe, please try again later.'; const UNDEFINED_PROPERTY_ERROR = 'EasyPost Notice: Undefined property of %s instance: %s'; const NO_MATCHING_MOCK_REQUEST = 'No matching mock request found for %s %s'; const END_OF_PAGINATION = 'There are no more pages to retrieve.'; diff --git a/lib/EasyPost/Service/ReferralCustomerService.php b/lib/EasyPost/Service/ReferralCustomerService.php index 9ff66f32..c8a94993 100644 --- a/lib/EasyPost/Service/ReferralCustomerService.php +++ b/lib/EasyPost/Service/ReferralCustomerService.php @@ -3,14 +3,9 @@ namespace EasyPost\Service; use EasyPost\Http\HttpMethod; -use EasyPost\Constant\Constants; use EasyPost\EasyPostClient; -use EasyPost\Exception\Api\ExternalApiException; -use EasyPost\Exception\Api\HttpException; -use EasyPost\Exception\Api\TimeoutException; use EasyPost\Http\Requestor; use EasyPost\Util\InternalUtil; -use GuzzleHttp\Client; /** * ReferralCustomer service containing all the logic to make API calls. @@ -71,49 +66,6 @@ public function updateEmail(string $userId, string $email): void Requestor::request($this->client, HttpMethod::PUT, "/referral_customers/{$userId}", $wrappedParams); } - /** - * Add a credit card to EasyPost for a ReferralCustomer without needing a Stripe account. - * - * This function requires the Referral User's API key. - * - * @param string $referralApiKey - * @param string $number - * @param int $expirationMonth - * @param int $expirationYear - * @param string $cvc - * @param string $priority - * @return mixed - * @throws ExternalApiException - */ - public function addCreditCard( - string $referralApiKey, - string $number, - int $expirationMonth, - int $expirationYear, - string $cvc, - string $priority = 'primary' - ): mixed { - $easypostStripeApiKey = self::retrieveEasypostStripeApiKey(); - - try { - $stripeToken = self::createStripeToken( - $number, - $expirationMonth, - $expirationYear, - $cvc, - $easypostStripeApiKey - ); - } catch (\Exception $error) { - throw new ExternalApiException(Constants::SEND_STRIPE_DETAILS_ERROR); - } - - $stripeToken = $stripeToken['id'] ?? ''; - - $response = self::createEasypostCreditCard($referralApiKey, $stripeToken, $priority); - - return InternalUtil::convertToEasyPostObject($this->client, $response); - } - /** * Add a credit card to EasyPost for a ReferralCustomer with a payment method ID from Stripe. * @@ -176,97 +128,10 @@ public function addBankAccountFromStripe( * * @return string */ - private function retrieveEasypostStripeApiKey(): string + public function retrieveEasypostStripeApiKey(): string { $response = Requestor::request($this->client, HttpMethod::GET, '/partners/stripe_public_key'); return $response['public_key'] ?? ''; } - - /** - * Retrieves the public EasyPost Stripe API key. - * - * @param string $number - * @param int $expirationMonth - * @param int $expirationYear - * @param string $cvc - * @param string $easypostStripeKey - * @return mixed - * @throws HttpException - * @throws TimeoutException - */ - private function createStripeToken( - string $number, - int $expirationMonth, - int $expirationYear, - string $cvc, - string $easypostStripeKey - ): mixed { - $headers = [ - 'Content-Type' => 'application/x-www-form-urlencoded', - 'Authorization' => "Bearer $easypostStripeKey", - ]; - - $creditCardDetails = [ - 'card' => [ - 'number' => $number, - 'exp_month' => $expirationMonth, - 'exp_year' => $expirationYear, - 'cvc' => $cvc, - ] - ]; - - $url = 'https://api.stripe.com/v1/tokens'; - - $guzzleClient = new Client(); - - $requestOptions['query'] = $creditCardDetails; - $requestOptions['headers'] = $headers; - $requestOptions['http_errors'] = false; - - try { - $response = $guzzleClient->request(HttpMethod::POST->value, $url, $requestOptions); - } catch (\GuzzleHttp\Exception\ConnectException $error) { - throw new HttpException(sprintf(Constants::COMMUNICATION_ERROR, 'Stripe', $error->getMessage())); - } - - // Guzzle does not have a native way of catching timeout exceptions... - // If we don't have a response at this point, it's likely due to a timeout. - // @phpstan-ignore-next-line - if (!isset($response)) { - throw new TimeoutException(sprintf(Constants::NO_RESPONSE_ERROR, 'Stripe')); - } - - $responseBody = $response->getBody(); - $httpStatus = $response->getStatusCode(); - $response = Requestor::interpretResponse($responseBody, $httpStatus); - - return $response; - } - - /** - * Submit the Stripe credit card token to EasyPost. - * - * @param string $referralApiKey - * @param string $stripeObjectId - * @param string $priority - * @return mixed - */ - private function createEasypostCreditCard( - string $referralApiKey, - string $stripeObjectId, - string $priority = 'primary' - ): mixed { - $params = [ - 'credit_card' => [ - 'stripe_object_id' => $stripeObjectId, - 'priority' => $priority, - ] - ]; - - $client = new EasyPostClient($referralApiKey); - $response = Requestor::request($client, HttpMethod::POST, '/credit_cards', $params); - - return InternalUtil::convertToEasyPostObject($this->client, $response); - } } diff --git a/test/EasyPost/ReferralCustomerTest.php b/test/EasyPost/ReferralCustomerTest.php index e5bd09e8..aa8a3b3a 100644 --- a/test/EasyPost/ReferralCustomerTest.php +++ b/test/EasyPost/ReferralCustomerTest.php @@ -116,28 +116,6 @@ public function testUpdateEmail(): void } } - /** - * Test that we can add a credit card to a referral user. - * - * This test requires a partner user's production API key via PARTNER_USER_PROD_API_KEY - * as well as one of that user's referral's production API keys via REFERRAL_USER_PROD_API_KEY. - */ - public function testAddCreditCard(): void - { - TestUtil::setupCassette('referral_customers/addCreditCard.yml'); - - $creditCard = self::$client->referralCustomer->addCreditCard( - self::$referralUserProdApiKey, - Fixture::creditCardDetails()['number'], - Fixture::creditCardDetails()['expiration_month'], - Fixture::creditCardDetails()['expiration_year'], - Fixture::creditCardDetails()['cvc'] - ); - - $this->assertStringMatchesFormat('pm_%s', $creditCard->id); - $this->assertEquals('6170', $creditCard->last4); - } - /** * Test that we can add a credit card to a referral user. * @@ -186,4 +164,19 @@ public function testAddBankAccountFromStripe(): void ); } } + + /** + * Test that we can retrieve EasyPost's Stripe API key. + * + * This test requires a partner user's production API key via PARTNER_USER_PROD_API_KEY. + */ + public function testRetrieveEasypostStripeApiKey(): void + { + TestUtil::setupCassette('referral_customers/retrieveEasypostStripeApiKey.yml'); + + $publicKey = self::$client->referralCustomer->retrieveEasypostStripeApiKey(); + + $this->assertIsString($publicKey); + $this->assertStringStartsWith('pk_', $publicKey); + } } diff --git a/test/cassettes/referral_customers/addCreditCard.yml b/test/cassettes/referral_customers/addCreditCard.yml deleted file mode 100644 index 0287f234..00000000 --- a/test/cassettes/referral_customers/addCreditCard.yml +++ /dev/null @@ -1,242 +0,0 @@ - -- - request: - method: GET - url: 'https://api.easypost.com/v2/partners/stripe_public_key' - headers: - Host: api.easypost.com - Accept-Encoding: '' - Accept: application/json - Authorization: '' - Content-Type: application/json - User-Agent: '' - response: - status: - code: 200 - message: OK - headers: - x-frame-options: SAMEORIGIN - x-xss-protection: '1; mode=block' - x-content-type-options: nosniff - x-download-options: noopen - x-permitted-cross-domain-policies: none - referrer-policy: strict-origin-when-cross-origin - x-ep-request-uuid: 7452537966be7d74e78a13210032d5d5 - cache-control: 'private, no-cache, no-store' - pragma: no-cache - expires: '0' - content-type: 'application/json; charset=utf-8' - content-length: '49' - x-runtime: '0.024157' - x-node: bigweb53nuq - x-version-label: easypost-202408151917-1527448f18-master - x-backend: easypost - x-proxied: ['intlb3nuq c0f5e722d1', 'extlb1nuq b6e1b5034c'] - strict-transport-security: 'max-age=31536000; includeSubDomains; preload' - body: '{"public_key":"pk_x3JSr5eOVWNTLRej8cZDde9VQ0AT5"}' - curl_info: - url: 'https://api.easypost.com/v2/partners/stripe_public_key' - content_type: 'application/json; charset=utf-8' - http_code: 200 - header_size: 687 - request_size: 303 - filetime: -1 - ssl_verify_result: 0 - redirect_count: 0 - total_time: 0.237289 - namelookup_time: 0.006835 - connect_time: 0.07311 - pretransfer_time: 0.14141 - size_upload: 0.0 - size_download: 49.0 - speed_download: 206.0 - speed_upload: 0.0 - download_content_length: 49.0 - upload_content_length: 0.0 - starttransfer_time: 0.237257 - redirect_time: 0.0 - redirect_url: '' - primary_ip: 169.62.110.131 - certinfo: { } - primary_port: 443 - local_ip: 10.130.6.11 - local_port: 56900 - http_version: 2 - protocol: 2 - ssl_verifyresult: 0 - scheme: https - appconnect_time_us: 141370 - connect_time_us: 73110 - namelookup_time_us: 6835 - pretransfer_time_us: 141410 - redirect_time_us: 0 - starttransfer_time_us: 237257 - total_time_us: 237289 - effective_method: GET - capath: '' - cainfo: '' - index: 0 -- - request: - method: POST - url: 'https://api.stripe.com/v1/tokens?' - headers: - Host: api.stripe.com - Content-Length: '0' - Accept-Encoding: '' - User-Agent: '' - Content-Type: application/x-www-form-urlencoded - Authorization: '' - Accept: '' - response: - status: - code: 200 - message: OK - headers: - Server: nginx - Date: 'Thu, 15 Aug 2024 22:13:09 GMT' - Content-Type: application/json - Content-Length: '788' - Connection: keep-alive - Access-Control-Allow-Credentials: 'true' - Access-Control-Allow-Methods: 'GET,HEAD,PUT,PATCH,POST,DELETE' - Access-Control-Allow-Origin: '*' - Access-Control-Expose-Headers: 'Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required' - Access-Control-Max-Age: '300' - Cache-Control: 'no-cache, no-store' - Content-Security-Policy: "report-uri https://q.stripe.com/csp-report?p=v1%2Ftokens; block-all-mixed-content; default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; img-src 'self'; script-src 'self' 'report-sample'; style-src 'self'" - Cross-Origin-Opener-Policy-Report-Only: 'same-origin; report-to="coop"' - Idempotency-Key: b7950341-1c11-4064-8836-4c9f8d97b88a - Original-Request: req_CpQ1fVpRwL3hF1 - Report-To: '{"group":"coop","max_age":8640,"endpoints":[{"url":"https://q.stripe.com/coop-report?s=payins-bapi-srv"}],"include_subdomains":true}' - Reporting-Endpoints: 'coop="https://q.stripe.com/coop-report?s=payins-bapi-srv"' - Request-Id: req_CpQ1fVpRwL3hF1 - Stripe-Should-Retry: 'false' - Stripe-Version: '2020-08-27' - Vary: Origin - X-Content-Type-Options: nosniff - X-Stripe-Priority-Routing-Enabled: 'true' - X-Stripe-Routing-Context-Priority-Tier: livemode-critical - Strict-Transport-Security: 'max-age=63072000; includeSubDomains; preload' - body: '{"id":"tok_0PoBurDqT4huGUvdO75MxSbj","object":"token","card":{"id":"card_0PoBurDqT4huGUvdT6xZ1Ast","object":"card","address_city":null,"address_country":null,"address_line1":null,"address_line1_check":null,"address_line2":null,"address_state":null,"address_zip":null,"address_zip_check":null,"brand":"Visa","country":"US","cvc_check":"unchecked","dynamic_last4":null,"exp_month":5,"exp_year":2028,"funding":"credit","last4":"6170","name":null,"networks":{"preferred":null},"tokenization_method":null,"wallet":null},"client_ip":"","created":1723759989,"livemode":true,"type":"card","used":false}' - curl_info: - url: 'https://api.stripe.com/v1/tokens?' - content_type: application/json - http_code: 200 - header_size: 1459 - request_size: 298 - filetime: -1 - ssl_verify_result: 0 - redirect_count: 0 - total_time: 0.477697 - namelookup_time: 0.031546 - connect_time: 0.074758 - pretransfer_time: 0.12075 - size_upload: 0.0 - size_download: 788.0 - speed_download: 1649.0 - speed_upload: 0.0 - download_content_length: 788.0 - upload_content_length: 0.0 - starttransfer_time: 0.47767 - redirect_time: 0.0 - redirect_url: '' - primary_ip: 52.26.14.11 - certinfo: { } - primary_port: 443 - local_ip: 192.168.1.75 - local_port: 56901 - http_version: 2 - protocol: 2 - ssl_verifyresult: 0 - scheme: https - appconnect_time_us: 120684 - connect_time_us: 74758 - namelookup_time_us: 31546 - pretransfer_time_us: 120750 - redirect_time_us: 0 - starttransfer_time_us: 477670 - total_time_us: 477697 - effective_method: POST - capath: '' - cainfo: '' - index: 0 -- - request: - method: POST - url: 'https://api.easypost.com/v2/credit_cards' - headers: - Host: api.easypost.com - Expect: '' - Accept-Encoding: '' - Accept: application/json - Authorization: '' - Content-Type: application/json - User-Agent: '' - body: '{"credit_card":{"stripe_object_id":"tok_0PoBurDqT4huGUvdO75MxSbj","priority":"primary"}}' - response: - status: - code: 201 - message: Created - headers: - x-frame-options: SAMEORIGIN - x-xss-protection: '1; mode=block' - x-content-type-options: nosniff - x-download-options: noopen - x-permitted-cross-domain-policies: none - referrer-policy: strict-origin-when-cross-origin - x-ep-request-uuid: 7452537966be7d75e78a13230032d663 - cache-control: 'private, no-cache, no-store' - pragma: no-cache - expires: '0' - content-type: 'application/json; charset=utf-8' - content-length: '193' - x-runtime: '2.793479' - x-node: bigweb33nuq - x-version-label: easypost-202408151917-1527448f18-master - x-backend: easypost - x-proxied: ['intlb3nuq c0f5e722d1', 'extlb1nuq b6e1b5034c'] - strict-transport-security: 'max-age=31536000; includeSubDomains; preload' - body: '{"id":"pm_53cc66088ef3440fbf36fa46964da578","disabled_at":null,"object":"CreditCard","name":null,"last4":"6170","exp_month":5,"exp_year":2028,"brand":"Visa","requires_mandate_collection":false}' - curl_info: - url: 'https://api.easypost.com/v2/credit_cards' - content_type: 'application/json; charset=utf-8' - http_code: 201 - header_size: 693 - request_size: 398 - filetime: -1 - ssl_verify_result: 0 - redirect_count: 0 - total_time: 2.996136 - namelookup_time: 0.00197 - connect_time: 0.067254 - pretransfer_time: 0.134764 - size_upload: 88.0 - size_download: 193.0 - speed_download: 64.0 - speed_upload: 29.0 - download_content_length: 193.0 - upload_content_length: 88.0 - starttransfer_time: 2.996086 - redirect_time: 0.0 - redirect_url: '' - primary_ip: 169.62.110.131 - certinfo: { } - primary_port: 443 - local_ip: 10.130.6.11 - local_port: 56902 - http_version: 2 - protocol: 2 - ssl_verifyresult: 0 - scheme: https - appconnect_time_us: 134701 - connect_time_us: 67254 - namelookup_time_us: 1970 - pretransfer_time_us: 134764 - redirect_time_us: 0 - starttransfer_time_us: 2996086 - total_time_us: 2996136 - effective_method: POST - capath: '' - cainfo: '' - index: 0 diff --git a/test/cassettes/referral_customers/retrieveEasypostStripeApiKey.yml b/test/cassettes/referral_customers/retrieveEasypostStripeApiKey.yml new file mode 100644 index 00000000..3cfe52e5 --- /dev/null +++ b/test/cassettes/referral_customers/retrieveEasypostStripeApiKey.yml @@ -0,0 +1,77 @@ +- + request: + method: GET + url: 'https://api.easypost.com/v2/partners/stripe_public_key' + headers: + Host: api.easypost.com + Accept-Encoding: '' + Accept: application/json + Authorization: '' + Content-Type: application/json + User-Agent: '' + response: + status: + code: 200 + message: OK + headers: + x-frame-options: SAMEORIGIN + x-xss-protection: '1; mode=block' + x-content-type-options: nosniff + x-download-options: noopen + x-permitted-cross-domain-policies: none + referrer-policy: strict-origin-when-cross-origin + x-ep-request-uuid: 91d0a0fa6ab4301ce2b97bb400af2fd1 + cache-control: 'private, no-cache, no-store' + pragma: no-cache + expires: '0' + content-type: 'application/json; charset=utf-8' + content-length: '49' + x-runtime: '0.029468' + x-node: web103azw + x-version-label: easypost-202609231716-41b3efb4ec-main + x-backend: easypost + x-proxied: ['intlb3azw f29267e751', 'extlb2azw 07fb4d8f06'] + strict-transport-security: 'max-age=31536000; includeSubDomains; preload' + body: '{"public_key":"pk_x3JSr5eOVWNTLRej8cZDde9VQ0AT5"}' + curl_info: + url: 'https://api.easypost.com/v2/partners/stripe_public_key' + content_type: 'application/json; charset=utf-8' + http_code: 200 + header_size: 695 + request_size: 310 + filetime: -1 + ssl_verify_result: 0 + redirect_count: 0 + total_time: 0.091 + namelookup_time: 0.001 + connect_time: 0.026 + pretransfer_time: 0.046 + size_upload: 0.0 + size_download: 49.0 + speed_download: 538.0 + speed_upload: 0.0 + download_content_length: 49.0 + upload_content_length: 0.0 + starttransfer_time: 0.091 + redirect_time: 0.0 + redirect_url: '' + primary_ip: 169.62.110.130 + certinfo: { } + primary_port: 443 + local_ip: 192.168.1.75 + local_port: 52830 + http_version: 2 + protocol: 2 + ssl_verifyresult: 0 + scheme: https + appconnect_time_us: 46000 + connect_time_us: 26000 + namelookup_time_us: 1000 + pretransfer_time_us: 46000 + redirect_time_us: 0 + starttransfer_time_us: 91000 + total_time_us: 91000 + effective_method: GET + capath: '' + cainfo: '' + index: 0