Skip to content

Commit a33eeb5

Browse files
Justintime50claude
andauthored
feat: add FedEx multi-factor authentication registration support (#539)
Ports the work from EasyPost/easypost-java#367 to here. --------- Co-authored-by: Claude Sonnet 4.5 <[email protected]>
1 parent 02f4b63 commit a33eeb5

File tree

7 files changed

+508
-1
lines changed

7 files changed

+508
-1
lines changed

examples

Submodule examples updated 60 files

src/easypost.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import CustomsItemService from './services/customs_item_service';
2323
import EmbeddableService from './services/embeddable_service';
2424
import EndShipperService from './services/end_shipper_service';
2525
import EventService from './services/event_service';
26+
import FedExRegistrationService from './services/fedex_registration_service';
2627
import InsuranceService from './services/insurance_service';
2728
import LumaService from './services/luma_service';
2829
import OrderService from './services/order_service';
@@ -375,6 +376,7 @@ EasyPostClient.SERVICES = {
375376
Embeddable: EmbeddableService,
376377
EndShipper: EndShipperService,
377378
Event: EventService,
379+
FedExRegistration: FedExRegistrationService,
378380
Insurance: InsuranceService,
379381
Luma: LumaService,
380382
Order: OrderService,
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { v4 as uuid } from 'uuid';
2+
3+
import baseService from './base_service';
4+
5+
export default (easypostClient) =>
6+
/**
7+
* The FedExRegistrationService class provides methods for registering FedEx carrier accounts with MFA.
8+
* @param {EasyPostClient} easypostClient - The pre-configured EasyPostClient instance to use for API requests with this service.
9+
*/
10+
class FedExRegistrationService extends baseService(easypostClient) {
11+
/**
12+
* Register the billing address for a FedEx account.
13+
* @param {string} fedexAccountNumber - The FedEx account number.
14+
* @param {Object} params - Map of parameters.
15+
* @returns {Object}
16+
*/
17+
static async registerAddress(fedexAccountNumber, params) {
18+
const wrappedParams = this._wrapAddressValidation(params);
19+
const endpoint = `fedex_registrations/${fedexAccountNumber}/address`;
20+
21+
try {
22+
const response = await easypostClient._post(endpoint, wrappedParams);
23+
return this._convertToEasyPostObject(response.body, params);
24+
} catch (e) {
25+
return Promise.reject(e);
26+
}
27+
}
28+
29+
/**
30+
* Request a PIN for FedEx account verification.
31+
* @param {string} fedexAccountNumber - The FedEx account number.
32+
* @param {string} pinMethodOption - The PIN delivery method: "SMS", "CALL", or "EMAIL".
33+
* @returns {Object}
34+
*/
35+
static async requestPin(fedexAccountNumber, pinMethodOption) {
36+
const wrappedParams = {
37+
pin_method: {
38+
option: pinMethodOption,
39+
},
40+
};
41+
const endpoint = `fedex_registrations/${fedexAccountNumber}/pin`;
42+
43+
try {
44+
const response = await easypostClient._post(endpoint, wrappedParams);
45+
return this._convertToEasyPostObject(response.body, wrappedParams);
46+
} catch (e) {
47+
return Promise.reject(e);
48+
}
49+
}
50+
51+
/**
52+
* Validate the PIN entered by the user for FedEx account verification.
53+
* @param {string} fedexAccountNumber - The FedEx account number.
54+
* @param {Object} params - Map of parameters.
55+
* @returns {Object}
56+
*/
57+
static async validatePin(fedexAccountNumber, params) {
58+
const wrappedParams = this._wrapPinValidation(params);
59+
const endpoint = `fedex_registrations/${fedexAccountNumber}/pin/validate`;
60+
61+
try {
62+
const response = await easypostClient._post(endpoint, wrappedParams);
63+
return this._convertToEasyPostObject(response.body, params);
64+
} catch (e) {
65+
return Promise.reject(e);
66+
}
67+
}
68+
69+
/**
70+
* Submit invoice information to complete FedEx account registration.
71+
* @param {string} fedexAccountNumber - The FedEx account number.
72+
* @param {Object} params - Map of parameters.
73+
* @returns {Object}
74+
*/
75+
static async submitInvoice(fedexAccountNumber, params) {
76+
const wrappedParams = this._wrapInvoiceValidation(params);
77+
const endpoint = `fedex_registrations/${fedexAccountNumber}/invoice`;
78+
79+
try {
80+
const response = await easypostClient._post(endpoint, wrappedParams);
81+
return this._convertToEasyPostObject(response.body, params);
82+
} catch (e) {
83+
return Promise.reject(e);
84+
}
85+
}
86+
87+
/**
88+
* Wraps address validation parameters and ensures the "name" field exists.
89+
* If not present, generates a UUID (with hyphens removed) as the name.
90+
* @private
91+
* @param {Object} params - The original parameters map.
92+
* @returns {Object} - A new map with properly wrapped address_validation and easypost_details.
93+
*/
94+
static _wrapAddressValidation(params) {
95+
const wrappedParams = {};
96+
97+
if (params.address_validation) {
98+
const addressValidation = { ...params.address_validation };
99+
this._ensureNameField(addressValidation);
100+
wrappedParams.address_validation = addressValidation;
101+
}
102+
103+
if (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+
* @private
114+
* @param {Object} params - The original parameters map.
115+
* @returns {Object} - A new map with properly wrapped pin_validation and easypost_details.
116+
*/
117+
static _wrapPinValidation(params) {
118+
const wrappedParams = {};
119+
120+
if (params.pin_validation) {
121+
const pinValidation = { ...params.pin_validation };
122+
this._ensureNameField(pinValidation);
123+
wrappedParams.pin_validation = pinValidation;
124+
}
125+
126+
if (params.easypost_details) {
127+
wrappedParams.easypost_details = params.easypost_details;
128+
}
129+
130+
return wrappedParams;
131+
}
132+
133+
/**
134+
* Wraps invoice validation parameters and ensures the "name" field exists.
135+
* If not present, generates a UUID (with hyphens removed) as the name.
136+
* @private
137+
* @param {Object} params - The original parameters map.
138+
* @returns {Object} - A new map with properly wrapped invoice_validation and easypost_details.
139+
*/
140+
static _wrapInvoiceValidation(params) {
141+
const wrappedParams = {};
142+
143+
if (params.invoice_validation) {
144+
const invoiceValidation = { ...params.invoice_validation };
145+
this._ensureNameField(invoiceValidation);
146+
wrappedParams.invoice_validation = invoiceValidation;
147+
}
148+
149+
if (params.easypost_details) {
150+
wrappedParams.easypost_details = params.easypost_details;
151+
}
152+
153+
return wrappedParams;
154+
}
155+
156+
/**
157+
* Ensures the "name" field exists in the provided map.
158+
* If not present, generates a UUID (with hyphens removed) as the name.
159+
* This follows the pattern used in the web UI implementation.
160+
* @private
161+
* @param {Object} map - The map to ensure the "name" field in.
162+
*/
163+
static _ensureNameField(map) {
164+
if (!map.name || map.name === null) {
165+
const uuidValue = uuid().replace(/-/g, '');
166+
map.name = uuidValue;
167+
}
168+
}
169+
};
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { expect } from 'chai';
2+
3+
import EasyPostClient from '../../src/easypost';
4+
import {
5+
MockMiddleware,
6+
MockRequest,
7+
MockRequestMatchRule,
8+
MockRequestResponseInfo,
9+
} from '../helpers/mocking';
10+
11+
/* eslint-disable func-names */
12+
describe('FedExRegistrationService', function () {
13+
it('registers a billing address', async function () {
14+
const fedexAccountNumber = '123456789';
15+
const addressValidation = {
16+
name: 'BILLING NAME',
17+
street1: '1234 BILLING STREET',
18+
city: 'BILLINGCITY',
19+
state: 'ST',
20+
postal_code: '12345',
21+
country_code: 'US',
22+
};
23+
24+
const easypostDetails = {
25+
carrier_account_id: 'ca_123',
26+
};
27+
28+
const params = {
29+
address_validation: addressValidation,
30+
easypost_details: easypostDetails,
31+
};
32+
33+
const mockResponse = {
34+
email_address: null,
35+
options: ['SMS', 'CALL', 'INVOICE'],
36+
phone_number: '***-***-9721',
37+
};
38+
39+
const middleware = (request) => {
40+
return new MockMiddleware(request, [
41+
new MockRequest(
42+
new MockRequestMatchRule(
43+
'POST',
44+
`v2\\/fedex_registrations\\/${fedexAccountNumber}\\/address`,
45+
),
46+
new MockRequestResponseInfo(200, mockResponse),
47+
),
48+
]);
49+
};
50+
51+
const client = new EasyPostClient('test_api_key', {
52+
requestMiddleware: middleware,
53+
});
54+
55+
const response = await client.FedExRegistration.registerAddress(fedexAccountNumber, params);
56+
57+
expect(response.email_address).to.be.null;
58+
expect(response.options).to.include('SMS');
59+
expect(response.options).to.include('CALL');
60+
expect(response.options).to.include('INVOICE');
61+
expect(response.phone_number).to.equal('***-***-9721');
62+
});
63+
64+
it('requests a pin', async function () {
65+
const fedexAccountNumber = '123456789';
66+
67+
const mockResponse = {
68+
message: 'sent secured Pin',
69+
};
70+
71+
const middleware = (request) => {
72+
return new MockMiddleware(request, [
73+
new MockRequest(
74+
new MockRequestMatchRule(
75+
'POST',
76+
`v2\\/fedex_registrations\\/${fedexAccountNumber}\\/pin`,
77+
),
78+
new MockRequestResponseInfo(200, mockResponse),
79+
),
80+
]);
81+
};
82+
83+
const client = new EasyPostClient('test_api_key', {
84+
requestMiddleware: middleware,
85+
});
86+
87+
const response = await client.FedExRegistration.requestPin(fedexAccountNumber, 'SMS');
88+
89+
expect(response.message).to.equal('sent secured Pin');
90+
});
91+
92+
it('validates a pin', async function () {
93+
const fedexAccountNumber = '123456789';
94+
const pinValidation = {
95+
pin_code: '123456',
96+
name: 'BILLING NAME',
97+
};
98+
99+
const easypostDetails = {
100+
carrier_account_id: 'ca_123',
101+
};
102+
103+
const params = {
104+
pin_validation: pinValidation,
105+
easypost_details: easypostDetails,
106+
};
107+
108+
const mockResponse = {
109+
id: 'ca_123',
110+
object: 'CarrierAccount',
111+
type: 'FedexAccount',
112+
credentials: {
113+
account_number: '123456789',
114+
mfa_key: '123456789-XXXXX',
115+
},
116+
};
117+
118+
const middleware = (request) => {
119+
return new MockMiddleware(request, [
120+
new MockRequest(
121+
new MockRequestMatchRule(
122+
'POST',
123+
`v2\\/fedex_registrations\\/${fedexAccountNumber}\\/pin\\/validate`,
124+
),
125+
new MockRequestResponseInfo(200, mockResponse),
126+
),
127+
]);
128+
};
129+
130+
const client = new EasyPostClient('test_api_key', {
131+
requestMiddleware: middleware,
132+
});
133+
134+
const response = await client.FedExRegistration.validatePin(fedexAccountNumber, params);
135+
136+
expect(response.id).to.equal('ca_123');
137+
expect(response.object).to.equal('CarrierAccount');
138+
expect(response.type).to.equal('FedexAccount');
139+
expect(response.credentials.account_number).to.equal('123456789');
140+
expect(response.credentials.mfa_key).to.equal('123456789-XXXXX');
141+
});
142+
143+
it('submits details about an invoice', async function () {
144+
const fedexAccountNumber = '123456789';
145+
const invoiceValidation = {
146+
name: 'BILLING NAME',
147+
invoice_number: 'INV-12345',
148+
invoice_date: '2025-12-08',
149+
invoice_amount: '100.00',
150+
invoice_currency: 'USD',
151+
};
152+
153+
const easypostDetails = {
154+
carrier_account_id: 'ca_123',
155+
};
156+
157+
const params = {
158+
invoice_validation: invoiceValidation,
159+
easypost_details: easypostDetails,
160+
};
161+
162+
const mockResponse = {
163+
id: 'ca_123',
164+
object: 'CarrierAccount',
165+
type: 'FedexAccount',
166+
credentials: {
167+
account_number: '123456789',
168+
mfa_key: '123456789-XXXXX',
169+
},
170+
};
171+
172+
const middleware = (request) => {
173+
return new MockMiddleware(request, [
174+
new MockRequest(
175+
new MockRequestMatchRule(
176+
'POST',
177+
`v2\\/fedex_registrations\\/${fedexAccountNumber}\\/invoice`,
178+
),
179+
new MockRequestResponseInfo(200, mockResponse),
180+
),
181+
]);
182+
};
183+
184+
const client = new EasyPostClient('test_api_key', {
185+
requestMiddleware: middleware,
186+
});
187+
188+
const response = await client.FedExRegistration.submitInvoice(fedexAccountNumber, params);
189+
190+
expect(response.id).to.equal('ca_123');
191+
expect(response.object).to.equal('CarrierAccount');
192+
expect(response.type).to.equal('FedexAccount');
193+
expect(response.credentials.account_number).to.equal('123456789');
194+
expect(response.credentials.mfa_key).to.equal('123456789-XXXXX');
195+
});
196+
});

0 commit comments

Comments
 (0)