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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- 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
- Adds support for `FedexDefaultAccount`, `GofoWalletAccount`, `UpsDapAccount`, `UniUniWalletAccount`, and `VehoWalletAccount` by properly routing carrier-account create requests to the `/carrier_accounts/open` endpoint

## v8.8.3 (2026-08-26)

Expand Down
30 changes: 19 additions & 11 deletions lib/EasyPost/Constant/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,25 @@ abstract class Constants
const SUPPORT_EMAIL = 'support@easypost.com';

// Validation
const CARRIER_ACCOUNT_TYPES_WITH_CUSTOM_WORKFLOWS = [
'FedexAccount',
'FedexSmartpostAccount'
];

const CARRIER_ACCOUNT_TYPES_WITH_CUSTOM_OAUTH = [
'AmazonShippingAccount',
'UpsAccount',
'UpsMailInnovationsAccount',
'UpsSurepostAccount',
'UspsShipAccount',
const CARRIER_ACCOUNT_CREATE_ENDPOINT_TYPES = [
'/carrier_accounts/open' => [
'FedexDefaultAccount',
'GofoWalletAccount',
'UpsDapAccount',
'UniUniWalletAccount',
'VehoWalletAccount',
],
'/carrier_accounts/register' => [
'FedexAccount',
'FedexSmartpostAccount',
],
'/carrier_accounts/register_oauth' => [
'AmazonShippingAccount',
'UpsAccount',
'UpsMailInnovationsAccount',
'UpsSurepostAccount',
'UspsShipAccount',
],
];

// Exception messages (many of these are intended to be used with `sprintf()`)
Expand Down
16 changes: 8 additions & 8 deletions lib/EasyPost/Service/CarrierAccountService.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ public function create(mixed $params = null): mixed
}

$carrierAccountType = $params['type'];
$params = [self::selectTopLayerKey($carrierAccountType) => $params];
$url = self::selectCarrierAccountCreationEndpoint($carrierAccountType);
$params = [self::selectTopLayerKey($url) => $params];
$response = Requestor::request($this->client, HttpMethod::POST, $url, $params);

return InternalUtil::convertToEasyPostObject($this->client, $response);
Expand All @@ -103,10 +103,10 @@ public function types(mixed $params = null): mixed
*/
private function selectCarrierAccountCreationEndpoint(string $carrierAccountType): string
{
if (in_array($carrierAccountType, Constants::CARRIER_ACCOUNT_TYPES_WITH_CUSTOM_WORKFLOWS, true)) {
return '/carrier_accounts/register';
} else if (in_array($carrierAccountType, Constants::CARRIER_ACCOUNT_TYPES_WITH_CUSTOM_OAUTH, true)) {
return '/carrier_accounts/register_oauth';
foreach (Constants::CARRIER_ACCOUNT_CREATE_ENDPOINT_TYPES as $endpoint => $carrierAccountTypes) {
if (in_array($carrierAccountType, $carrierAccountTypes, true)) {
return $endpoint;
}
}

return '/carrier_accounts';
Expand All @@ -115,12 +115,12 @@ private function selectCarrierAccountCreationEndpoint(string $carrierAccountType
/**
* Select the top-key layer for creating/updating a carrier account based on the type of carrier account.
*
* @param string $carrierAccountType The type of carrier account to create.
* @param string $carrierAccountUrl The URL used for the carrier-account create endpoint.
* @return string The top-layer key for creating/updating a carrier account.
*/
private function selectTopLayerKey(string $carrierAccountType): string
private function selectTopLayerKey(string $carrierAccountUrl): string
{
if (in_array($carrierAccountType, Constants::CARRIER_ACCOUNT_TYPES_WITH_CUSTOM_OAUTH, true)) {
if ($carrierAccountUrl === '/carrier_accounts/register_oauth') {
return 'carrier_account_oauth_registrations';
} else {
return 'carrier_account';
Expand Down
152 changes: 62 additions & 90 deletions test/EasyPost/CarrierAccountTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
namespace EasyPost\Test;

use EasyPost\CarrierAccount;
use EasyPost\Constant\Constants;
use EasyPost\EasyPostClient;
use EasyPost\Exception\Api\ApiException;
use EasyPost\Exception\General\MissingParameterException;
use PHPUnit\Framework\TestCase;

Expand All @@ -30,60 +30,50 @@ public static function tearDownAfterClass(): void
}

/**
* Test creating a carrier account.
* @return array<string, array<mixed>>
*/
public function testCreate(): void
public static function carrierAccountCreateUrlRoutingDataProvider(): array
{
TestUtil::setupCassette('carrier_accounts/create.yml');

self::$client = new EasyPostClient((string)getenv('EASYPOST_PROD_API_KEY'));

$carrierAccount = self::$client->carrierAccount->create(Fixture::basicCarrierAccount());

$this->assertEquals('DhlEcsAccount', $carrierAccount->type);
$this->assertInstanceOf(CarrierAccount::class, $carrierAccount);
$this->assertStringMatchesFormat('ca_%s', $carrierAccount->id);
$tests = [];

foreach (Constants::CARRIER_ACCOUNT_CREATE_ENDPOINT_TYPES as $endpoint => $carrierAccountTypes) {
foreach ($carrierAccountTypes as $carrierAccountType) {
$tests[$carrierAccountType] = [
$carrierAccountType,
$endpoint,
self::carrierCreatePayloadForEndpoint($endpoint),
];
}
}

// Delete the carrier account once it's done being tested.
self::$client->carrierAccount->delete($carrierAccount->id);
return $tests;
}

/**
* Test creating a UPS account.
* @param string $endpoint
* @return array<string, mixed>
*/
public function testCreateUps(): void
private static function carrierCreatePayloadForEndpoint(string $endpoint): array
{
TestUtil::setupCassette('carrier_accounts/create_ups.yml');

self::$client = new EasyPostClient((string)getenv('EASYPOST_PROD_API_KEY'));

$carrierAccount = self::$client->carrierAccount->create([
'type' => 'UpsAccount',
'account_number' => '123456789'
]);

$this->assertEquals('UpsAccount', $carrierAccount->type);
$this->assertInstanceOf(CarrierAccount::class, $carrierAccount);
$this->assertStringMatchesFormat('ca_%s', $carrierAccount->id);
if ($endpoint === '/carrier_accounts/open' || $endpoint === '/carrier_accounts/register') {
return ['registration_data' => []];
}

// Delete the carrier account once it's done being tested.
self::$client->carrierAccount->delete($carrierAccount->id);
return [];
}

/**
* Test creating an Amazon account.
* Test creating a carrier account.
*/
public function testCreateAmazon(): void
public function testCreate(): void
{
TestUtil::setupCassette('carrier_accounts/create_amazon.yml');
TestUtil::setupCassette('carrier_accounts/create.yml');

self::$client = new EasyPostClient((string)getenv('EASYPOST_PROD_API_KEY'));

$carrierAccount = self::$client->carrierAccount->create([
'type' => 'AmazonShippingAccount',
]);
$carrierAccount = self::$client->carrierAccount->create(Fixture::basicCarrierAccount());

$this->assertEquals('AmazonShippingAccount', $carrierAccount->type);
$this->assertEquals('DhlEcsAccount', $carrierAccount->type);
$this->assertInstanceOf(CarrierAccount::class, $carrierAccount);
$this->assertStringMatchesFormat('ca_%s', $carrierAccount->id);

Expand Down Expand Up @@ -112,40 +102,49 @@ public function testCreateWithoutType(): void
}

/**
* Test creating a carrier account with custom workflow such as FedEx or UPS.
* Test creating carrier accounts with custom endpoint routing.
*
* We purposefully don't pass data here because real data is required for this endpoint
* which we don't have in a test context, simply assert the error matches when no data is passed.
* @dataProvider carrierAccountCreateUrlRoutingDataProvider
* @param array<string, mixed> $params
*/
public function testCreateWithCustomWorkflow(): void
{
TestUtil::setupCassette('carrier_accounts/createWithCustomWorkflow.yml');

public function testCreateUrlRouting(
string $carrierAccountType,
string $expectedEndpoint,
array $params
): void {
$capturedRequest = [];
TestUtil::setupCassette("carrier_accounts/create_url_routing_{$carrierAccountType}.yml");
self::$client = new EasyPostClient((string)getenv('EASYPOST_PROD_API_KEY'));
self::$client->requestEvent->addHandler(function (array $requestInfo) use (&$capturedRequest): void {
if (!isset($capturedRequest['path']) && str_contains($requestInfo['path'], '/carrier_accounts')) {
$capturedRequest = $requestInfo;
}
});

$data = [];
$data['type'] = 'FedexAccount';
// We have to send some registration data, otherwise API will throw a 400 Bad Request
$data['registration_data'] = ['some' => 'data'];

$errorFound = false;

$payload = ['type' => $carrierAccountType] + $params;
$carrierAccount = null;
try {
$carrierAccount = self::$client->carrierAccount->create($data);
// Delete the carrier account once it's done being tested (should not be reached).
self::$client->carrierAccount->delete($carrierAccount->id);
} catch (ApiException $error) {
$this->assertEquals(422, $error->getHttpStatus());
$errors = $error->errors;
foreach ($errors as $error) {
if ($error['field'] == 'account_number' && $error['message'] == 'must be present and a string') {
$errorFound = true;
break;
}
$carrierAccount = self::$client->carrierAccount->create($payload);
} catch (\Exception $error) {
if ($error instanceof \EasyPost\Exception\Api\ApiException) {
$this->assertNotContains($error->getHttpStatus(), [404, 500]);
} else {
throw $error;
}
}

$this->assertTrue($errorFound);
if ($carrierAccount !== null) {
$this->assertInstanceOf(CarrierAccount::class, $carrierAccount);
$this->assertStringMatchesFormat('ca_%s', $carrierAccount->id);
self::$client->carrierAccount->delete($carrierAccount->id);
}

$this->assertEquals(Constants::API_BASE . '/v2' . $expectedEndpoint, $capturedRequest['path']);
if ($expectedEndpoint === '/carrier_accounts/register_oauth') {
$this->assertArrayHasKey('carrier_account_oauth_registrations', $capturedRequest['request_body']);
} else {
$this->assertArrayHasKey('carrier_account', $capturedRequest['request_body']);
}
}

/**
Expand Down Expand Up @@ -182,33 +181,6 @@ public function testAll(): void
$this->assertContainsOnlyInstancesOf(CarrierAccount::class, $carrierAccounts);
}

/**
* Test updating an UPS account.
*/
public function testUpdateUps(): void
{
TestUtil::setupCassette('carrier_accounts/update_ups.yml');

self::$client = new EasyPostClient((string)getenv('EASYPOST_PROD_API_KEY'));

$carrierAccount = self::$client->carrierAccount->create([
'type' => 'UpsAccount',
'account_number' => '123456789'
]);

$updatedCarrierAccount = self::$client->carrierAccount->update(
$carrierAccount->id,
['account_number' => '987654321']
);

$this->assertInstanceOf(CarrierAccount::class, $updatedCarrierAccount);
$this->assertStringMatchesFormat('ca_%s', $updatedCarrierAccount->id);
$this->assertEquals('UpsAccount', $updatedCarrierAccount->type);

// Delete the carrier account once it's done being tested.
self::$client->carrierAccount->delete($updatedCarrierAccount->id);
}

/**
* Test updating a carrier account.
*/
Expand Down
Loading
Loading