Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion lib/EasyPost/Constant/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.';
Expand Down
137 changes: 1 addition & 136 deletions lib/EasyPost/Service/ReferralCustomerService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
}
}
37 changes: 15 additions & 22 deletions test/EasyPost/ReferralCustomerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
}
}
Loading
Loading