From 60b1ef952d7dab099614deaa0e3925cbb9a71c68 Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:49:08 -0700 Subject: [PATCH 1/3] 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 --- src/easypost.js | 2 + src/services/fedex_registration_service.js | 170 ++++++++++++++++ test/services/fedex_registration.test.js | 187 ++++++++++++++++++ types/EasyPost.d.ts | 2 + .../FedExRegistration/FedExRegistration.d.ts | 137 +++++++++++++ types/FedExRegistration/index.d.ts | 1 + 6 files changed, 499 insertions(+) create mode 100644 src/services/fedex_registration_service.js create mode 100644 test/services/fedex_registration.test.js create mode 100644 types/FedExRegistration/FedExRegistration.d.ts create mode 100644 types/FedExRegistration/index.d.ts diff --git a/src/easypost.js b/src/easypost.js index 5ca27cdd..117137de 100644 --- a/src/easypost.js +++ b/src/easypost.js @@ -23,6 +23,7 @@ import CustomsItemService from './services/customs_item_service'; import EmbeddableService from './services/embeddable_service'; import EndShipperService from './services/end_shipper_service'; import EventService from './services/event_service'; +import FedExRegistrationService from './services/fedex_registration_service'; import InsuranceService from './services/insurance_service'; import LumaService from './services/luma_service'; import OrderService from './services/order_service'; @@ -375,6 +376,7 @@ EasyPostClient.SERVICES = { Embeddable: EmbeddableService, EndShipper: EndShipperService, Event: EventService, + FedExRegistration: FedExRegistrationService, Insurance: InsuranceService, Luma: LumaService, Order: OrderService, diff --git a/src/services/fedex_registration_service.js b/src/services/fedex_registration_service.js new file mode 100644 index 00000000..ba0ecfa7 --- /dev/null +++ b/src/services/fedex_registration_service.js @@ -0,0 +1,170 @@ +import { v4 as uuid } from 'uuid'; + +import baseService from './base_service'; + +export default (easypostClient) => + /** + * The FedExRegistrationService class provides methods for registering FedEx carrier accounts with MFA. + * @param {EasyPostClient} easypostClient - The pre-configured EasyPostClient instance to use for API requests with this service. + */ + class FedExRegistrationService extends baseService(easypostClient) { + /** + * Register the billing address for a FedEx account. + * Advanced method for custom parameter structures. + * @param {string} fedexAccountNumber - The FedEx account number. + * @param {Object} params - Map of parameters. + * @returns {Object} - FedExAccountValidationResponse object with next steps (PIN or invoice validation). + */ + static async registerAddress(fedexAccountNumber, params) { + const wrappedParams = this._wrapAddressValidation(params); + const endpoint = `fedex_registrations/${fedexAccountNumber}/address`; + + try { + const response = await easypostClient._post(endpoint, wrappedParams); + return this._convertToEasyPostObject(response.body, params); + } catch (e) { + return Promise.reject(e); + } + } + + /** + * Request a PIN for FedEx account verification. + * @param {string} fedexAccountNumber - The FedEx account number. + * @param {string} pinMethodOption - The PIN delivery method: "SMS", "CALL", or "EMAIL". + * @returns {Object} - FedExRequestPinResponse object confirming PIN was sent. + */ + static async requestPin(fedexAccountNumber, pinMethodOption) { + const wrappedParams = { + pin_method: { + option: pinMethodOption, + }, + }; + const endpoint = `fedex_registrations/${fedexAccountNumber}/pin`; + + try { + const response = await easypostClient._post(endpoint, wrappedParams); + return this._convertToEasyPostObject(response.body, wrappedParams); + } catch (e) { + return Promise.reject(e); + } + } + + /** + * Validate the PIN entered by the user for FedEx account verification. + * @param {string} fedexAccountNumber - The FedEx account number. + * @param {Object} params - Map of parameters. + * @returns {Object} - FedExAccountValidationResponse object. + */ + static async validatePin(fedexAccountNumber, params) { + const wrappedParams = this._wrapPinValidation(params); + const endpoint = `fedex_registrations/${fedexAccountNumber}/pin/validate`; + + try { + const response = await easypostClient._post(endpoint, wrappedParams); + return this._convertToEasyPostObject(response.body, params); + } catch (e) { + return Promise.reject(e); + } + } + + /** + * Submit invoice information to complete FedEx account registration. + * @param {string} fedexAccountNumber - The FedEx account number. + * @param {Object} params - Map of parameters. + * @returns {Object} - FedExAccountValidationResponse object. + */ + static async submitInvoice(fedexAccountNumber, params) { + const wrappedParams = this._wrapInvoiceValidation(params); + const endpoint = `fedex_registrations/${fedexAccountNumber}/invoice`; + + try { + const response = await easypostClient._post(endpoint, wrappedParams); + return this._convertToEasyPostObject(response.body, params); + } catch (e) { + return Promise.reject(e); + } + } + + /** + * Wraps address validation parameters and ensures the "name" field exists. + * If not present, generates a UUID (with hyphens removed) as the name. + * @private + * @param {Object} params - The original parameters map. + * @returns {Object} - A new map with properly wrapped address_validation and easypost_details. + */ + static _wrapAddressValidation(params) { + const wrappedParams = {}; + + if (params.address_validation) { + const addressValidation = { ...params.address_validation }; + this._ensureNameField(addressValidation); + wrappedParams.address_validation = addressValidation; + } + + if (params.easypost_details) { + wrappedParams.easypost_details = params.easypost_details; + } + + return wrappedParams; + } + + /** + * Wraps PIN validation parameters and ensures the "name" field exists. + * If not present, generates a UUID (with hyphens removed) as the name. + * @private + * @param {Object} params - The original parameters map. + * @returns {Object} - A new map with properly wrapped pin_validation and easypost_details. + */ + static _wrapPinValidation(params) { + const wrappedParams = {}; + + if (params.pin_validation) { + const pinValidation = { ...params.pin_validation }; + this._ensureNameField(pinValidation); + wrappedParams.pin_validation = pinValidation; + } + + if (params.easypost_details) { + wrappedParams.easypost_details = params.easypost_details; + } + + return wrappedParams; + } + + /** + * Wraps invoice validation parameters and ensures the "name" field exists. + * If not present, generates a UUID (with hyphens removed) as the name. + * @private + * @param {Object} params - The original parameters map. + * @returns {Object} - A new map with properly wrapped invoice_validation and easypost_details. + */ + static _wrapInvoiceValidation(params) { + const wrappedParams = {}; + + if (params.invoice_validation) { + const invoiceValidation = { ...params.invoice_validation }; + this._ensureNameField(invoiceValidation); + wrappedParams.invoice_validation = invoiceValidation; + } + + if (params.easypost_details) { + wrappedParams.easypost_details = params.easypost_details; + } + + return wrappedParams; + } + + /** + * Ensures the "name" field exists in the provided map. + * If not present, generates a UUID (with hyphens removed) as the name. + * This follows the pattern used in the web UI implementation. + * @private + * @param {Object} map - The map to ensure the "name" field in. + */ + static _ensureNameField(map) { + if (!map.name || map.name === null) { + const uuidValue = uuid().replace(/-/g, ''); + map.name = uuidValue; + } + } + }; diff --git a/test/services/fedex_registration.test.js b/test/services/fedex_registration.test.js new file mode 100644 index 00000000..4a136ad3 --- /dev/null +++ b/test/services/fedex_registration.test.js @@ -0,0 +1,187 @@ +import { expect } from 'chai'; + +import EasyPostClient from '../../src/easypost'; +import { + MockMiddleware, + MockRequest, + MockRequestMatchRule, + MockRequestResponseInfo, +} from '../helpers/mocking'; + +/* eslint-disable func-names */ +describe('FedExRegistrationService', function () { + it('registers a billing address', async function () { + const fedexAccountNumber = '123456789'; + const addressValidation = { + name: 'BILLING NAME', + street1: '1234 BILLING STREET', + city: 'BILLINGCITY', + state: 'ST', + postal_code: '12345', + country_code: 'US', + }; + + const easypostDetails = { + carrier_account_id: 'ca_123', + }; + + const params = { + address_validation: addressValidation, + easypost_details: easypostDetails, + }; + + const mockResponse = { + email_address: null, + options: ['SMS', 'CALL', 'INVOICE'], + phone_number: '***-***-9721', + }; + + const middleware = (request) => { + return new MockMiddleware(request, [ + new MockRequest( + new MockRequestMatchRule('POST', `v2\\/fedex_registrations\\/${fedexAccountNumber}\\/address`), + new MockRequestResponseInfo(200, mockResponse), + ), + ]); + }; + + const client = new EasyPostClient('test_api_key', { + requestMiddleware: middleware, + }); + + const response = await client.FedExRegistration.registerAddress(fedexAccountNumber, params); + + expect(response.email_address).to.be.null; + expect(response.options).to.include('SMS'); + expect(response.options).to.include('CALL'); + expect(response.options).to.include('INVOICE'); + expect(response.phone_number).to.equal('***-***-9721'); + }); + + it('requests a pin', async function () { + const fedexAccountNumber = '123456789'; + + const mockResponse = { + message: 'sent secured Pin', + }; + + const middleware = (request) => { + return new MockMiddleware(request, [ + new MockRequest( + new MockRequestMatchRule('POST', `v2\\/fedex_registrations\\/${fedexAccountNumber}\\/pin`), + new MockRequestResponseInfo(200, mockResponse), + ), + ]); + }; + + const client = new EasyPostClient('test_api_key', { + requestMiddleware: middleware, + }); + + const response = await client.FedExRegistration.requestPin(fedexAccountNumber, 'SMS'); + + expect(response.message).to.equal('sent secured Pin'); + }); + + it('validates a pin', async function () { + const fedexAccountNumber = '123456789'; + const pinValidation = { + pin_code: '123456', + name: 'BILLING NAME', + }; + + const easypostDetails = { + carrier_account_id: 'ca_123', + }; + + const params = { + pin_validation: pinValidation, + easypost_details: easypostDetails, + }; + + const mockResponse = { + id: 'ca_123', + object: 'CarrierAccount', + type: 'FedexAccount', + credentials: { + account_number: '123456789', + mfa_key: '123456789-XXXXX', + }, + }; + + const middleware = (request) => { + return new MockMiddleware(request, [ + new MockRequest( + new MockRequestMatchRule( + 'POST', + `v2\\/fedex_registrations\\/${fedexAccountNumber}\\/pin\\/validate`, + ), + new MockRequestResponseInfo(200, mockResponse), + ), + ]); + }; + + const client = new EasyPostClient('test_api_key', { + requestMiddleware: middleware, + }); + + const response = await client.FedExRegistration.validatePin(fedexAccountNumber, params); + + expect(response.id).to.equal('ca_123'); + expect(response.object).to.equal('CarrierAccount'); + expect(response.type).to.equal('FedexAccount'); + expect(response.credentials.account_number).to.equal('123456789'); + expect(response.credentials.mfa_key).to.equal('123456789-XXXXX'); + }); + + it('submits details about an invoice', async function () { + const fedexAccountNumber = '123456789'; + const invoiceValidation = { + name: 'BILLING NAME', + invoice_number: 'INV-12345', + invoice_date: '2025-12-08', + invoice_amount: '100.00', + invoice_currency: 'USD', + }; + + const easypostDetails = { + carrier_account_id: 'ca_123', + }; + + const params = { + invoice_validation: invoiceValidation, + easypost_details: easypostDetails, + }; + + const mockResponse = { + id: 'ca_123', + object: 'CarrierAccount', + type: 'FedexAccount', + credentials: { + account_number: '123456789', + mfa_key: '123456789-XXXXX', + }, + }; + + const middleware = (request) => { + return new MockMiddleware(request, [ + new MockRequest( + new MockRequestMatchRule('POST', `v2\\/fedex_registrations\\/${fedexAccountNumber}\\/invoice`), + new MockRequestResponseInfo(200, mockResponse), + ), + ]); + }; + + const client = new EasyPostClient('test_api_key', { + requestMiddleware: middleware, + }); + + const response = await client.FedExRegistration.submitInvoice(fedexAccountNumber, params); + + expect(response.id).to.equal('ca_123'); + expect(response.object).to.equal('CarrierAccount'); + expect(response.type).to.equal('FedexAccount'); + expect(response.credentials.account_number).to.equal('123456789'); + expect(response.credentials.mfa_key).to.equal('123456789-XXXXX'); + }); +}); diff --git a/types/EasyPost.d.ts b/types/EasyPost.d.ts index f51ac406..2d5af80b 100644 --- a/types/EasyPost.d.ts +++ b/types/EasyPost.d.ts @@ -10,6 +10,7 @@ import { CustomsInfo, CustomsItem } from './Customs'; import { EmbeddablesSession } from './Embeddable'; import { EndShipper } from './EndShipper'; import { Event } from './Event'; +import { FedExRegistration } from './FedExRegistration'; import { Fee } from './Fee'; import { Insurance } from './Insurance'; import { Luma } from './Luma'; @@ -89,6 +90,7 @@ export default class EasyPost { public Embeddable: typeof EmbeddablesSession; public EndShipper: typeof EndShipper; public Event: typeof Event; + public FedExRegistration: typeof FedExRegistration; public Fee: typeof Fee; // TODO: Fix IFee public Insurance: typeof Insurance; public Luma: typeof Luma; diff --git a/types/FedExRegistration/FedExRegistration.d.ts b/types/FedExRegistration/FedExRegistration.d.ts new file mode 100644 index 00000000..fc1999cd --- /dev/null +++ b/types/FedExRegistration/FedExRegistration.d.ts @@ -0,0 +1,137 @@ +/** + * Interface for FedEx Account Validation Response returned during initial validation steps. + */ +export declare interface IFedExAccountValidationResponse { + /** + * Email address for PIN delivery (if applicable) + */ + email_address?: string | null; + + /** + * Available PIN delivery options (SMS, CALL, EMAIL, INVOICE) + */ + options?: Array | null; + + /** + * Phone number for PIN delivery (if applicable) + */ + phone_number?: string | null; + + /** + * Carrier account ID (returned upon successful validation) + */ + id?: string | null; + + /** + * Object type (returned upon successful validation) + */ + object?: string | null; + + /** + * Carrier account type (returned upon successful validation) + */ + type?: string | null; + + /** + * Carrier account credentials (returned upon successful validation) + */ + credentials?: Record | null; +} + +/** + * Interface for FedEx Request PIN Response. + */ +export declare interface IFedExRequestPinResponse { + /** + * Confirmation message that PIN was sent + */ + message?: string | null; +} + +/** + * The FedEx Registration service provides methods for registering FedEx carrier accounts with multi-factor authentication. + * + * @see https://support.easypost.com/hc/en-us/articles/35262738410253-FedEx-Multi-Factor-Authentication + */ +declare class FedExRegistration { + /** + * Register the billing address for a FedEx account. + * + * @param fedexAccountNumber The FedEx account number. + * @param params Parameters including address_validation and optional easypost_details. + * @returns {Promise} Response object with next steps (PIN or invoice validation). + */ + static registerAddress( + fedexAccountNumber: string, + params: { + address_validation: { + name?: string; + street1: string; + city: string; + state: string; + postal_code: string; + country_code: string; + }; + easypost_details?: { + carrier_account_id: string; + }; + }, + ): Promise; + + /** + * Request a PIN for FedEx account verification. + * + * @param fedexAccountNumber The FedEx account number. + * @param pinMethodOption The PIN delivery method: "SMS", "CALL", or "EMAIL". + * @returns {Promise} Response object confirming PIN was sent. + */ + static requestPin( + fedexAccountNumber: string, + pinMethodOption: string, + ): Promise; + + /** + * Validate the PIN entered by the user for FedEx account verification. + * + * @param fedexAccountNumber The FedEx account number. + * @param params Parameters including pin_validation and optional easypost_details. + * @returns {Promise} Response object with carrier account credentials. + */ + static validatePin( + fedexAccountNumber: string, + params: { + pin_validation: { + pin_code: string; + name?: string; + }; + easypost_details?: { + carrier_account_id: string; + }; + }, + ): Promise; + + /** + * Submit invoice information to complete FedEx account registration. + * + * @param fedexAccountNumber The FedEx account number. + * @param params Parameters including invoice_validation and optional easypost_details. + * @returns {Promise} Response object with carrier account credentials. + */ + static submitInvoice( + fedexAccountNumber: string, + params: { + invoice_validation: { + name?: string; + invoice_number: string; + invoice_date: string; + invoice_amount: string; + invoice_currency: string; + }; + easypost_details?: { + carrier_account_id: string; + }; + }, + ): Promise; +} + +export type { FedExRegistration }; diff --git a/types/FedExRegistration/index.d.ts b/types/FedExRegistration/index.d.ts new file mode 100644 index 00000000..3279c547 --- /dev/null +++ b/types/FedExRegistration/index.d.ts @@ -0,0 +1 @@ +export * from './FedExRegistration'; From e580cea2b3742717ef90f28260c07a31a521a5a7 Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:10:48 -0700 Subject: [PATCH 2/3] docs: remove incorrect comments --- src/services/fedex_registration_service.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/services/fedex_registration_service.js b/src/services/fedex_registration_service.js index ba0ecfa7..9d12c780 100644 --- a/src/services/fedex_registration_service.js +++ b/src/services/fedex_registration_service.js @@ -10,10 +10,9 @@ export default (easypostClient) => class FedExRegistrationService extends baseService(easypostClient) { /** * Register the billing address for a FedEx account. - * Advanced method for custom parameter structures. * @param {string} fedexAccountNumber - The FedEx account number. * @param {Object} params - Map of parameters. - * @returns {Object} - FedExAccountValidationResponse object with next steps (PIN or invoice validation). + * @returns {Object} */ static async registerAddress(fedexAccountNumber, params) { const wrappedParams = this._wrapAddressValidation(params); @@ -31,7 +30,7 @@ export default (easypostClient) => * Request a PIN for FedEx account verification. * @param {string} fedexAccountNumber - The FedEx account number. * @param {string} pinMethodOption - The PIN delivery method: "SMS", "CALL", or "EMAIL". - * @returns {Object} - FedExRequestPinResponse object confirming PIN was sent. + * @returns {Object} */ static async requestPin(fedexAccountNumber, pinMethodOption) { const wrappedParams = { @@ -53,7 +52,7 @@ export default (easypostClient) => * Validate the PIN entered by the user for FedEx account verification. * @param {string} fedexAccountNumber - The FedEx account number. * @param {Object} params - Map of parameters. - * @returns {Object} - FedExAccountValidationResponse object. + * @returns {Object} */ static async validatePin(fedexAccountNumber, params) { const wrappedParams = this._wrapPinValidation(params); @@ -71,7 +70,7 @@ export default (easypostClient) => * Submit invoice information to complete FedEx account registration. * @param {string} fedexAccountNumber - The FedEx account number. * @param {Object} params - Map of parameters. - * @returns {Object} - FedExAccountValidationResponse object. + * @returns {Object} */ static async submitInvoice(fedexAccountNumber, params) { const wrappedParams = this._wrapInvoiceValidation(params); From 476a3ffe7c4c1b09c3e01df3c1e08c28f07cc918 Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:44:35 -0700 Subject: [PATCH 3/3] fix: lint --- examples | 2 +- test/services/fedex_registration.test.js | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/examples b/examples index 739f96c8..18cab0e9 160000 --- a/examples +++ b/examples @@ -1 +1 @@ -Subproject commit 739f96c80fa211060e0a0b8c13e7371c9a09d66f +Subproject commit 18cab0e968e370eebf124db5a32b202bffbdda9f diff --git a/test/services/fedex_registration.test.js b/test/services/fedex_registration.test.js index 4a136ad3..e22da1b8 100644 --- a/test/services/fedex_registration.test.js +++ b/test/services/fedex_registration.test.js @@ -39,7 +39,10 @@ describe('FedExRegistrationService', function () { const middleware = (request) => { return new MockMiddleware(request, [ new MockRequest( - new MockRequestMatchRule('POST', `v2\\/fedex_registrations\\/${fedexAccountNumber}\\/address`), + new MockRequestMatchRule( + 'POST', + `v2\\/fedex_registrations\\/${fedexAccountNumber}\\/address`, + ), new MockRequestResponseInfo(200, mockResponse), ), ]); @@ -68,7 +71,10 @@ describe('FedExRegistrationService', function () { const middleware = (request) => { return new MockMiddleware(request, [ new MockRequest( - new MockRequestMatchRule('POST', `v2\\/fedex_registrations\\/${fedexAccountNumber}\\/pin`), + new MockRequestMatchRule( + 'POST', + `v2\\/fedex_registrations\\/${fedexAccountNumber}\\/pin`, + ), new MockRequestResponseInfo(200, mockResponse), ), ]); @@ -166,7 +172,10 @@ describe('FedExRegistrationService', function () { const middleware = (request) => { return new MockMiddleware(request, [ new MockRequest( - new MockRequestMatchRule('POST', `v2\\/fedex_registrations\\/${fedexAccountNumber}\\/invoice`), + new MockRequestMatchRule( + 'POST', + `v2\\/fedex_registrations\\/${fedexAccountNumber}\\/invoice`, + ), new MockRequestResponseInfo(200, mockResponse), ), ]);