Skip to content

Commit 10d7952

Browse files
Justintime50claude
andauthored
feat(fedex): add FedEx Multi-Factor Authentication endpoints (#379)
Ports the work from EasyPost/easypost-java#367 to here. --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 1fc514c commit 10d7952

3 files changed

Lines changed: 351 additions & 0 deletions

File tree

lib/EasyPost/EasyPostClient.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use EasyPost\Service\EmbeddableService;
2323
use EasyPost\Service\EndShipperService;
2424
use EasyPost\Service\EventService;
25+
use EasyPost\Service\FedExRegistrationService;
2526
use EasyPost\Service\InsuranceService;
2627
use EasyPost\Service\LumaService;
2728
use EasyPost\Service\OrderService;
@@ -58,6 +59,7 @@
5859
* @property EmbeddableService $embeddable
5960
* @property EndShipperService $endShipper
6061
* @property EventService $event
62+
* @property FedExRegistrationService $fedexRegistration
6163
* @property InsuranceService $insurance
6264
* @property LumaService $luma
6365
* @property OrderService $order
@@ -134,6 +136,7 @@ public function __get(string $serviceName)
134136
'embeddable' => EmbeddableService::class,
135137
'endShipper' => EndShipperService::class,
136138
'event' => EventService::class,
139+
'fedexRegistration' => FedExRegistrationService::class,
137140
'insurance' => InsuranceService::class,
138141
'luma' => LumaService::class,
139142
'order' => OrderService::class,
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<?php
2+
3+
namespace EasyPost\Service;
4+
5+
use EasyPost\Http\Requestor;
6+
use EasyPost\Util\InternalUtil;
7+
8+
/**
9+
* FedExRegistration service containing all the logic to make API calls.
10+
*/
11+
class FedExRegistrationService extends BaseService
12+
{
13+
/**
14+
* Register the billing address for a FedEx account.
15+
* Advanced method for custom parameter structures.
16+
*
17+
* @param string $fedexAccountNumber
18+
* @param mixed $params
19+
* @return mixed
20+
*/
21+
public function registerAddress(string $fedexAccountNumber, mixed $params = null): mixed
22+
{
23+
$wrappedParams = $this->wrapAddressValidation($params);
24+
$url = "/fedex_registrations/{$fedexAccountNumber}/address";
25+
26+
$response = Requestor::request($this->client, 'post', $url, $wrappedParams);
27+
28+
return InternalUtil::convertToEasyPostObject($this->client, $response);
29+
}
30+
31+
/**
32+
* Request a PIN for FedEx account verification.
33+
*
34+
* @param string $fedexAccountNumber
35+
* @param string $pinMethodOption
36+
* @return mixed
37+
*/
38+
public function requestPin(string $fedexAccountNumber, string $pinMethodOption): mixed
39+
{
40+
$wrappedParams = [
41+
'pin_method' => [
42+
'option' => $pinMethodOption,
43+
],
44+
];
45+
$url = "/fedex_registrations/{$fedexAccountNumber}/pin";
46+
47+
$response = Requestor::request($this->client, 'post', $url, $wrappedParams);
48+
49+
return InternalUtil::convertToEasyPostObject($this->client, $response);
50+
}
51+
52+
/**
53+
* Validate the PIN entered by the user for FedEx account verification.
54+
*
55+
* @param string $fedexAccountNumber
56+
* @param mixed $params
57+
* @return mixed
58+
*/
59+
public function validatePin(string $fedexAccountNumber, mixed $params = null): mixed
60+
{
61+
$wrappedParams = $this->wrapPinValidation($params);
62+
$url = "/fedex_registrations/{$fedexAccountNumber}/pin/validate";
63+
64+
$response = Requestor::request($this->client, 'post', $url, $wrappedParams);
65+
66+
return InternalUtil::convertToEasyPostObject($this->client, $response);
67+
}
68+
69+
/**
70+
* Submit invoice information to complete FedEx account registration.
71+
*
72+
* @param string $fedexAccountNumber
73+
* @param mixed $params
74+
* @return mixed
75+
*/
76+
public function submitInvoice(string $fedexAccountNumber, mixed $params = null): mixed
77+
{
78+
$wrappedParams = $this->wrapInvoiceValidation($params);
79+
$url = "/fedex_registrations/{$fedexAccountNumber}/invoice";
80+
81+
$response = Requestor::request($this->client, 'post', $url, $wrappedParams);
82+
83+
return InternalUtil::convertToEasyPostObject($this->client, $response);
84+
}
85+
86+
/**
87+
* Wraps address validation parameters and ensures the "name" field exists.
88+
* If not present, generates a UUID (with hyphens removed) as the name.
89+
*
90+
* @param mixed $params
91+
* @return array<string, mixed>
92+
*/
93+
private function wrapAddressValidation(mixed $params): array
94+
{
95+
$wrappedParams = [];
96+
97+
if (isset($params['address_validation'])) {
98+
$addressValidation = $params['address_validation'];
99+
$this->ensureNameField($addressValidation);
100+
$wrappedParams['address_validation'] = $addressValidation;
101+
}
102+
103+
if (isset($params['easypost_details'])) {
104+
$wrappedParams['easypost_details'] = $params['easypost_details'];
105+
}
106+
107+
return $wrappedParams;
108+
}
109+
110+
/**
111+
* Wraps PIN validation parameters and ensures the "name" field exists.
112+
* If not present, generates a UUID (with hyphens removed) as the name.
113+
*
114+
* @param mixed $params
115+
* @return array<string, mixed>
116+
*/
117+
private function wrapPinValidation(mixed $params): array
118+
{
119+
$wrappedParams = [];
120+
121+
if (isset($params['pin_validation'])) {
122+
$pinValidation = $params['pin_validation'];
123+
$this->ensureNameField($pinValidation);
124+
$wrappedParams['pin_validation'] = $pinValidation;
125+
}
126+
127+
if (isset($params['easypost_details'])) {
128+
$wrappedParams['easypost_details'] = $params['easypost_details'];
129+
}
130+
131+
return $wrappedParams;
132+
}
133+
134+
/**
135+
* Wraps invoice validation parameters and ensures the "name" field exists.
136+
* If not present, generates a UUID (with hyphens removed) as the name.
137+
*
138+
* @param mixed $params
139+
* @return array<string, mixed>
140+
*/
141+
private function wrapInvoiceValidation(mixed $params): array
142+
{
143+
$wrappedParams = [];
144+
145+
if (isset($params['invoice_validation'])) {
146+
$invoiceValidation = $params['invoice_validation'];
147+
$this->ensureNameField($invoiceValidation);
148+
$wrappedParams['invoice_validation'] = $invoiceValidation;
149+
}
150+
151+
if (isset($params['easypost_details'])) {
152+
$wrappedParams['easypost_details'] = $params['easypost_details'];
153+
}
154+
155+
return $wrappedParams;
156+
}
157+
158+
/**
159+
* Ensures the "name" field exists in the provided array.
160+
* If not present, generates a unique ID as the name.
161+
* This follows the pattern used in the web UI implementation.
162+
*
163+
* @param array<string, mixed> &$array
164+
* @return void
165+
*/
166+
private function ensureNameField(array &$array): void
167+
{
168+
if (!isset($array['name'])) {
169+
$array['name'] = uniqid();
170+
}
171+
}
172+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<?php
2+
3+
namespace EasyPost\Test;
4+
5+
use EasyPost\Constant\Constants;
6+
use EasyPost\EasyPostClient;
7+
use EasyPost\EasyPostObject;
8+
use EasyPost\Test\Mocking\MockingUtility;
9+
use EasyPost\Test\Mocking\MockRequest;
10+
use EasyPost\Test\Mocking\MockRequestMatchRule;
11+
use EasyPost\Test\Mocking\MockRequestResponseInfo;
12+
use PHPUnit\Framework\TestCase;
13+
14+
class FedExRegistrationTest extends TestCase
15+
{
16+
private static EasyPostClient $client;
17+
18+
/**
19+
* Setup the testing environment for this file.
20+
*/
21+
public static function setUpBeforeClass(): void
22+
{
23+
$mockingUtility = new MockingUtility(
24+
[
25+
new MockRequest(
26+
new MockRequestMatchRule(
27+
'post',
28+
'/v2\/fedex_registrations\/\S*\/address$/'
29+
),
30+
new MockRequestResponseInfo(
31+
200,
32+
'{"email_address":null,"options":["SMS","CALL","INVOICE"],"phone_number":"***-***-9721"}'
33+
)
34+
),
35+
new MockRequest(
36+
new MockRequestMatchRule(
37+
'post',
38+
'/v2\/fedex_registrations\/\S*\/pin$/'
39+
),
40+
new MockRequestResponseInfo(
41+
200,
42+
'{"message":"sent secured Pin"}'
43+
)
44+
),
45+
new MockRequest(
46+
new MockRequestMatchRule(
47+
'post',
48+
'/v2\/fedex_registrations\/\S*\/pin\/validate$/'
49+
),
50+
new MockRequestResponseInfo(
51+
200,
52+
'{"id":"ca_123","type":"FedexAccount",' .
53+
'"credentials":{"account_number":"123456789","mfa_key":"123456789-XXXXX"}}'
54+
)
55+
),
56+
new MockRequest(
57+
new MockRequestMatchRule(
58+
'post',
59+
'/v2\/fedex_registrations\/\S*\/invoice$/'
60+
),
61+
new MockRequestResponseInfo(
62+
200,
63+
'{"id":"ca_123","type":"FedexAccount",' .
64+
'"credentials":{"account_number":"123456789","mfa_key":"123456789-XXXXX"}}'
65+
)
66+
),
67+
]
68+
);
69+
70+
self::$client = new EasyPostClient(
71+
(string)getenv('EASYPOST_TEST_API_KEY'),
72+
Constants::TIMEOUT,
73+
Constants::API_BASE,
74+
$mockingUtility
75+
);
76+
}
77+
78+
/**
79+
* Test registering a billing address.
80+
*/
81+
public function testRegisterAddress(): void
82+
{
83+
$fedexAccountNumber = '123456789';
84+
$params = [
85+
'address_validation' => [
86+
'name' => 'BILLING NAME',
87+
'street1' => '1234 BILLING STREET',
88+
'city' => 'BILLINGCITY',
89+
'state' => 'ST',
90+
'postal_code' => '12345',
91+
'country_code' => 'US',
92+
],
93+
'easypost_details' => [
94+
'carrier_account_id' => 'ca_123',
95+
],
96+
];
97+
98+
$response = self::$client->fedexRegistration->registerAddress($fedexAccountNumber, $params);
99+
100+
$this->assertInstanceOf(EasyPostObject::class, $response);
101+
$this->assertNull($response->email_address); // @phpstan-ignore-line
102+
$this->assertNotNull($response->options); // @phpstan-ignore-line
103+
$this->assertContains('SMS', $response->options);
104+
$this->assertContains('CALL', $response->options);
105+
$this->assertContains('INVOICE', $response->options);
106+
$this->assertEquals('***-***-9721', $response->phone_number); // @phpstan-ignore-line
107+
}
108+
109+
/**
110+
* Test requesting a pin.
111+
*/
112+
public function testRequestPin(): void
113+
{
114+
$fedexAccountNumber = '123456789';
115+
116+
$response = self::$client->fedexRegistration->requestPin($fedexAccountNumber, 'SMS');
117+
118+
$this->assertInstanceOf(EasyPostObject::class, $response);
119+
$this->assertEquals('sent secured Pin', $response->message); // @phpstan-ignore-line
120+
}
121+
122+
/**
123+
* Test validating a pin.
124+
*/
125+
public function testValidatePin(): void
126+
{
127+
$fedexAccountNumber = '123456789';
128+
$params = [
129+
'pin_validation' => [
130+
'pin_code' => '123456',
131+
'name' => 'BILLING NAME',
132+
],
133+
'easypost_details' => [
134+
'carrier_account_id' => 'ca_123',
135+
],
136+
];
137+
138+
$response = self::$client->fedexRegistration->validatePin($fedexAccountNumber, $params);
139+
140+
$this->assertInstanceOf(EasyPostObject::class, $response);
141+
$this->assertEquals('ca_123', $response->id); // @phpstan-ignore-line
142+
$this->assertEquals('FedexAccount', $response->type); // @phpstan-ignore-line
143+
$this->assertNotNull($response->credentials); // @phpstan-ignore-line
144+
$this->assertEquals('123456789', $response->credentials['account_number']);
145+
$this->assertEquals('123456789-XXXXX', $response->credentials['mfa_key']);
146+
}
147+
148+
/**
149+
* Test submitting details about an invoice.
150+
*/
151+
public function testSubmitInvoice(): void
152+
{
153+
$fedexAccountNumber = '123456789';
154+
$params = [
155+
'invoice_validation' => [
156+
'name' => 'BILLING NAME',
157+
'invoice_number' => 'INV-12345',
158+
'invoice_date' => '2025-12-08',
159+
'invoice_amount' => '100.00',
160+
'invoice_currency' => 'USD',
161+
],
162+
'easypost_details' => [
163+
'carrier_account_id' => 'ca_123',
164+
],
165+
];
166+
167+
$response = self::$client->fedexRegistration->submitInvoice($fedexAccountNumber, $params);
168+
169+
$this->assertInstanceOf(EasyPostObject::class, $response);
170+
$this->assertEquals('ca_123', $response->id); // @phpstan-ignore-line
171+
$this->assertEquals('FedexAccount', $response->type); // @phpstan-ignore-line
172+
$this->assertNotNull($response->credentials); // @phpstan-ignore-line
173+
$this->assertEquals('123456789', $response->credentials['account_number']);
174+
$this->assertEquals('123456789-XXXXX', $response->credentials['mfa_key']);
175+
}
176+
}

0 commit comments

Comments
 (0)