Skip to content

Commit 60b1ef9

Browse files
Justintime50claude
andcommitted
feat: add FedEx multi-factor authentication registration support
Adds native FedEx 2FA registration support to the library, enabling programmatic carrier account registration. New methods: - fedexRegistration.registerAddress - fedexRegistration.requestPin - fedexRegistration.validatePin - fedexRegistration.submitInvoice Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent 02f4b63 commit 60b1ef9

File tree

6 files changed

+499
-0
lines changed

6 files changed

+499
-0
lines changed

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: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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+
* Advanced method for custom parameter structures.
14+
* @param {string} fedexAccountNumber - The FedEx account number.
15+
* @param {Object} params - Map of parameters.
16+
* @returns {Object} - FedExAccountValidationResponse object with next steps (PIN or invoice validation).
17+
*/
18+
static async registerAddress(fedexAccountNumber, params) {
19+
const wrappedParams = this._wrapAddressValidation(params);
20+
const endpoint = `fedex_registrations/${fedexAccountNumber}/address`;
21+
22+
try {
23+
const response = await easypostClient._post(endpoint, wrappedParams);
24+
return this._convertToEasyPostObject(response.body, params);
25+
} catch (e) {
26+
return Promise.reject(e);
27+
}
28+
}
29+
30+
/**
31+
* Request a PIN for FedEx account verification.
32+
* @param {string} fedexAccountNumber - The FedEx account number.
33+
* @param {string} pinMethodOption - The PIN delivery method: "SMS", "CALL", or "EMAIL".
34+
* @returns {Object} - FedExRequestPinResponse object confirming PIN was sent.
35+
*/
36+
static async requestPin(fedexAccountNumber, pinMethodOption) {
37+
const wrappedParams = {
38+
pin_method: {
39+
option: pinMethodOption,
40+
},
41+
};
42+
const endpoint = `fedex_registrations/${fedexAccountNumber}/pin`;
43+
44+
try {
45+
const response = await easypostClient._post(endpoint, wrappedParams);
46+
return this._convertToEasyPostObject(response.body, wrappedParams);
47+
} catch (e) {
48+
return Promise.reject(e);
49+
}
50+
}
51+
52+
/**
53+
* Validate the PIN entered by the user for FedEx account verification.
54+
* @param {string} fedexAccountNumber - The FedEx account number.
55+
* @param {Object} params - Map of parameters.
56+
* @returns {Object} - FedExAccountValidationResponse object.
57+
*/
58+
static async validatePin(fedexAccountNumber, params) {
59+
const wrappedParams = this._wrapPinValidation(params);
60+
const endpoint = `fedex_registrations/${fedexAccountNumber}/pin/validate`;
61+
62+
try {
63+
const response = await easypostClient._post(endpoint, wrappedParams);
64+
return this._convertToEasyPostObject(response.body, params);
65+
} catch (e) {
66+
return Promise.reject(e);
67+
}
68+
}
69+
70+
/**
71+
* Submit invoice information to complete FedEx account registration.
72+
* @param {string} fedexAccountNumber - The FedEx account number.
73+
* @param {Object} params - Map of parameters.
74+
* @returns {Object} - FedExAccountValidationResponse object.
75+
*/
76+
static async submitInvoice(fedexAccountNumber, params) {
77+
const wrappedParams = this._wrapInvoiceValidation(params);
78+
const endpoint = `fedex_registrations/${fedexAccountNumber}/invoice`;
79+
80+
try {
81+
const response = await easypostClient._post(endpoint, wrappedParams);
82+
return this._convertToEasyPostObject(response.body, params);
83+
} catch (e) {
84+
return Promise.reject(e);
85+
}
86+
}
87+
88+
/**
89+
* Wraps address validation parameters and ensures the "name" field exists.
90+
* If not present, generates a UUID (with hyphens removed) as the name.
91+
* @private
92+
* @param {Object} params - The original parameters map.
93+
* @returns {Object} - A new map with properly wrapped address_validation and easypost_details.
94+
*/
95+
static _wrapAddressValidation(params) {
96+
const wrappedParams = {};
97+
98+
if (params.address_validation) {
99+
const addressValidation = { ...params.address_validation };
100+
this._ensureNameField(addressValidation);
101+
wrappedParams.address_validation = addressValidation;
102+
}
103+
104+
if (params.easypost_details) {
105+
wrappedParams.easypost_details = params.easypost_details;
106+
}
107+
108+
return wrappedParams;
109+
}
110+
111+
/**
112+
* Wraps PIN validation parameters and ensures the "name" field exists.
113+
* If not present, generates a UUID (with hyphens removed) as the name.
114+
* @private
115+
* @param {Object} params - The original parameters map.
116+
* @returns {Object} - A new map with properly wrapped pin_validation and easypost_details.
117+
*/
118+
static _wrapPinValidation(params) {
119+
const wrappedParams = {};
120+
121+
if (params.pin_validation) {
122+
const pinValidation = { ...params.pin_validation };
123+
this._ensureNameField(pinValidation);
124+
wrappedParams.pin_validation = pinValidation;
125+
}
126+
127+
if (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+
* @private
138+
* @param {Object} params - The original parameters map.
139+
* @returns {Object} - A new map with properly wrapped invoice_validation and easypost_details.
140+
*/
141+
static _wrapInvoiceValidation(params) {
142+
const wrappedParams = {};
143+
144+
if (params.invoice_validation) {
145+
const invoiceValidation = { ...params.invoice_validation };
146+
this._ensureNameField(invoiceValidation);
147+
wrappedParams.invoice_validation = invoiceValidation;
148+
}
149+
150+
if (params.easypost_details) {
151+
wrappedParams.easypost_details = params.easypost_details;
152+
}
153+
154+
return wrappedParams;
155+
}
156+
157+
/**
158+
* Ensures the "name" field exists in the provided map.
159+
* If not present, generates a UUID (with hyphens removed) as the name.
160+
* This follows the pattern used in the web UI implementation.
161+
* @private
162+
* @param {Object} map - The map to ensure the "name" field in.
163+
*/
164+
static _ensureNameField(map) {
165+
if (!map.name || map.name === null) {
166+
const uuidValue = uuid().replace(/-/g, '');
167+
map.name = uuidValue;
168+
}
169+
}
170+
};
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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('POST', `v2\\/fedex_registrations\\/${fedexAccountNumber}\\/address`),
43+
new MockRequestResponseInfo(200, mockResponse),
44+
),
45+
]);
46+
};
47+
48+
const client = new EasyPostClient('test_api_key', {
49+
requestMiddleware: middleware,
50+
});
51+
52+
const response = await client.FedExRegistration.registerAddress(fedexAccountNumber, params);
53+
54+
expect(response.email_address).to.be.null;
55+
expect(response.options).to.include('SMS');
56+
expect(response.options).to.include('CALL');
57+
expect(response.options).to.include('INVOICE');
58+
expect(response.phone_number).to.equal('***-***-9721');
59+
});
60+
61+
it('requests a pin', async function () {
62+
const fedexAccountNumber = '123456789';
63+
64+
const mockResponse = {
65+
message: 'sent secured Pin',
66+
};
67+
68+
const middleware = (request) => {
69+
return new MockMiddleware(request, [
70+
new MockRequest(
71+
new MockRequestMatchRule('POST', `v2\\/fedex_registrations\\/${fedexAccountNumber}\\/pin`),
72+
new MockRequestResponseInfo(200, mockResponse),
73+
),
74+
]);
75+
};
76+
77+
const client = new EasyPostClient('test_api_key', {
78+
requestMiddleware: middleware,
79+
});
80+
81+
const response = await client.FedExRegistration.requestPin(fedexAccountNumber, 'SMS');
82+
83+
expect(response.message).to.equal('sent secured Pin');
84+
});
85+
86+
it('validates a pin', async function () {
87+
const fedexAccountNumber = '123456789';
88+
const pinValidation = {
89+
pin_code: '123456',
90+
name: 'BILLING NAME',
91+
};
92+
93+
const easypostDetails = {
94+
carrier_account_id: 'ca_123',
95+
};
96+
97+
const params = {
98+
pin_validation: pinValidation,
99+
easypost_details: easypostDetails,
100+
};
101+
102+
const mockResponse = {
103+
id: 'ca_123',
104+
object: 'CarrierAccount',
105+
type: 'FedexAccount',
106+
credentials: {
107+
account_number: '123456789',
108+
mfa_key: '123456789-XXXXX',
109+
},
110+
};
111+
112+
const middleware = (request) => {
113+
return new MockMiddleware(request, [
114+
new MockRequest(
115+
new MockRequestMatchRule(
116+
'POST',
117+
`v2\\/fedex_registrations\\/${fedexAccountNumber}\\/pin\\/validate`,
118+
),
119+
new MockRequestResponseInfo(200, mockResponse),
120+
),
121+
]);
122+
};
123+
124+
const client = new EasyPostClient('test_api_key', {
125+
requestMiddleware: middleware,
126+
});
127+
128+
const response = await client.FedExRegistration.validatePin(fedexAccountNumber, params);
129+
130+
expect(response.id).to.equal('ca_123');
131+
expect(response.object).to.equal('CarrierAccount');
132+
expect(response.type).to.equal('FedexAccount');
133+
expect(response.credentials.account_number).to.equal('123456789');
134+
expect(response.credentials.mfa_key).to.equal('123456789-XXXXX');
135+
});
136+
137+
it('submits details about an invoice', async function () {
138+
const fedexAccountNumber = '123456789';
139+
const invoiceValidation = {
140+
name: 'BILLING NAME',
141+
invoice_number: 'INV-12345',
142+
invoice_date: '2025-12-08',
143+
invoice_amount: '100.00',
144+
invoice_currency: 'USD',
145+
};
146+
147+
const easypostDetails = {
148+
carrier_account_id: 'ca_123',
149+
};
150+
151+
const params = {
152+
invoice_validation: invoiceValidation,
153+
easypost_details: easypostDetails,
154+
};
155+
156+
const mockResponse = {
157+
id: 'ca_123',
158+
object: 'CarrierAccount',
159+
type: 'FedexAccount',
160+
credentials: {
161+
account_number: '123456789',
162+
mfa_key: '123456789-XXXXX',
163+
},
164+
};
165+
166+
const middleware = (request) => {
167+
return new MockMiddleware(request, [
168+
new MockRequest(
169+
new MockRequestMatchRule('POST', `v2\\/fedex_registrations\\/${fedexAccountNumber}\\/invoice`),
170+
new MockRequestResponseInfo(200, mockResponse),
171+
),
172+
]);
173+
};
174+
175+
const client = new EasyPostClient('test_api_key', {
176+
requestMiddleware: middleware,
177+
});
178+
179+
const response = await client.FedExRegistration.submitInvoice(fedexAccountNumber, params);
180+
181+
expect(response.id).to.equal('ca_123');
182+
expect(response.object).to.equal('CarrierAccount');
183+
expect(response.type).to.equal('FedexAccount');
184+
expect(response.credentials.account_number).to.equal('123456789');
185+
expect(response.credentials.mfa_key).to.equal('123456789-XXXXX');
186+
});
187+
});

types/EasyPost.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { CustomsInfo, CustomsItem } from './Customs';
1010
import { EmbeddablesSession } from './Embeddable';
1111
import { EndShipper } from './EndShipper';
1212
import { Event } from './Event';
13+
import { FedExRegistration } from './FedExRegistration';
1314
import { Fee } from './Fee';
1415
import { Insurance } from './Insurance';
1516
import { Luma } from './Luma';
@@ -89,6 +90,7 @@ export default class EasyPost {
8990
public Embeddable: typeof EmbeddablesSession;
9091
public EndShipper: typeof EndShipper;
9192
public Event: typeof Event;
93+
public FedExRegistration: typeof FedExRegistration;
9294
public Fee: typeof Fee; // TODO: Fix IFee
9395
public Insurance: typeof Insurance;
9496
public Luma: typeof Luma;

0 commit comments

Comments
 (0)