Skip to content

Commit 95ba105

Browse files
Justintime50claude
andauthored
feat: add FedEx multi-factor authentication support (#341)
Ports the work from EasyPost/easypost-java#367 to here. --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 10a1478 commit 95ba105

File tree

4 files changed

+273
-0
lines changed

4 files changed

+273
-0
lines changed

lib/easypost/client.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def initialize(api_key:, read_timeout: 60, open_timeout: 30, api_base: 'https://
4949
EasyPost::Services::Embeddable,
5050
EasyPost::Services::EndShipper,
5151
EasyPost::Services::Event,
52+
EasyPost::Services::FedexRegistration,
5253
EasyPost::Services::Insurance,
5354
EasyPost::Services::Luma,
5455
EasyPost::Services::Order,

lib/easypost/services.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module EasyPost::Services
2121
require_relative 'services/embeddable'
2222
require_relative 'services/end_shipper'
2323
require_relative 'services/event'
24+
require_relative 'services/fedex_registration'
2425
require_relative 'services/insurance'
2526
require_relative 'services/luma'
2627
require_relative 'services/order'
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# frozen_string_literal: true
2+
3+
require 'securerandom'
4+
5+
class EasyPost::Services::FedexRegistration < EasyPost::Services::Service
6+
# Register the billing address for a FedEx account.
7+
def register_address(fedex_account_number, params = {})
8+
wrapped_params = wrap_address_validation(params)
9+
endpoint = "fedex_registrations/#{fedex_account_number}/address"
10+
11+
response = @client.make_request(:post, endpoint, wrapped_params)
12+
13+
EasyPost::InternalUtilities::Json.convert_json_to_object(response, EasyPost::Models::EasyPostObject)
14+
end
15+
16+
# Request a PIN for FedEx account verification.
17+
def request_pin(fedex_account_number, pin_method_option)
18+
wrapped_params = {
19+
pin_method: {
20+
option: pin_method_option,
21+
},
22+
}
23+
endpoint = "fedex_registrations/#{fedex_account_number}/pin"
24+
25+
response = @client.make_request(:post, endpoint, wrapped_params)
26+
27+
EasyPost::InternalUtilities::Json.convert_json_to_object(response, EasyPost::Models::EasyPostObject)
28+
end
29+
30+
# Validate the PIN entered by the user for FedEx account verification.
31+
def validate_pin(fedex_account_number, params = {})
32+
wrapped_params = wrap_pin_validation(params)
33+
endpoint = "fedex_registrations/#{fedex_account_number}/pin/validate"
34+
35+
response = @client.make_request(:post, endpoint, wrapped_params)
36+
37+
EasyPost::InternalUtilities::Json.convert_json_to_object(response, EasyPost::Models::EasyPostObject)
38+
end
39+
40+
# Submit invoice information to complete FedEx account registration.
41+
def submit_invoice(fedex_account_number, params = {})
42+
wrapped_params = wrap_invoice_validation(params)
43+
endpoint = "fedex_registrations/#{fedex_account_number}/invoice"
44+
45+
response = @client.make_request(:post, endpoint, wrapped_params)
46+
47+
EasyPost::InternalUtilities::Json.convert_json_to_object(response, EasyPost::Models::EasyPostObject)
48+
end
49+
50+
private
51+
52+
# Wraps address validation parameters and ensures the "name" field exists.
53+
# If not present, generates a UUID (with hyphens removed) as the name.
54+
def wrap_address_validation(params)
55+
wrapped_params = {}
56+
57+
if params.key?(:address_validation)
58+
address_validation = params[:address_validation].dup
59+
ensure_name_field(address_validation)
60+
wrapped_params[:address_validation] = address_validation
61+
end
62+
63+
wrapped_params[:easypost_details] = params[:easypost_details] if params.key?(:easypost_details)
64+
65+
wrapped_params
66+
end
67+
68+
# Wraps PIN validation parameters and ensures the "name" field exists.
69+
# If not present, generates a UUID (with hyphens removed) as the name.
70+
def wrap_pin_validation(params)
71+
wrapped_params = {}
72+
73+
if params.key?(:pin_validation)
74+
pin_validation = params[:pin_validation].dup
75+
ensure_name_field(pin_validation)
76+
wrapped_params[:pin_validation] = pin_validation
77+
end
78+
79+
wrapped_params[:easypost_details] = params[:easypost_details] if params.key?(:easypost_details)
80+
81+
wrapped_params
82+
end
83+
84+
# Wraps invoice validation parameters and ensures the "name" field exists.
85+
# If not present, generates a UUID (with hyphens removed) as the name.
86+
def wrap_invoice_validation(params)
87+
wrapped_params = {}
88+
89+
if params.key?(:invoice_validation)
90+
invoice_validation = params[:invoice_validation].dup
91+
ensure_name_field(invoice_validation)
92+
wrapped_params[:invoice_validation] = invoice_validation
93+
end
94+
95+
wrapped_params[:easypost_details] = params[:easypost_details] if params.key?(:easypost_details)
96+
97+
wrapped_params
98+
end
99+
100+
# Ensures the "name" field exists in the provided hash.
101+
# If not present, generates a UUID (with hyphens removed) as the name.
102+
# This follows the pattern used in the web UI implementation.
103+
def ensure_name_field(hash)
104+
return if hash.key?(:name) && !hash[:name].nil?
105+
106+
uuid = SecureRandom.uuid.delete('-')
107+
hash[:name] = uuid
108+
end
109+
end

spec/fedex_registration_spec.rb

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe EasyPost::Services::FedexRegistration do
6+
let(:client) { EasyPost::Client.new(api_key: ENV['EASYPOST_PROD_API_KEY']) }
7+
8+
describe '.register_address' do
9+
it 'registers a billing address' do
10+
fedex_account_number = '123456789'
11+
address_validation = {
12+
name: 'BILLING NAME',
13+
street1: '1234 BILLING STREET',
14+
city: 'BILLINGCITY',
15+
state: 'ST',
16+
postal_code: '12345',
17+
country_code: 'US',
18+
}
19+
easypost_details = {
20+
carrier_account_id: 'ca_123',
21+
}
22+
params = {
23+
address_validation: address_validation,
24+
easypost_details: easypost_details,
25+
}
26+
27+
json_response = {
28+
'email_address' => nil,
29+
'options' => %w[SMS CALL INVOICE],
30+
'phone_number' => '***-***-9721',
31+
}
32+
33+
allow(client).to receive(:make_request).with(
34+
:post,
35+
"fedex_registrations/#{fedex_account_number}/address",
36+
params,
37+
).and_return(json_response)
38+
39+
response = client.fedex_registration.register_address(fedex_account_number, params)
40+
41+
expect(response).to be_an_instance_of(EasyPost::Models::EasyPostObject)
42+
expect(response.email_address).to be_nil
43+
expect(response.options).to include('SMS')
44+
expect(response.options).to include('CALL')
45+
expect(response.options).to include('INVOICE')
46+
expect(response.phone_number).to eq('***-***-9721')
47+
end
48+
end
49+
50+
describe '.request_pin' do
51+
it 'requests a pin' do
52+
fedex_account_number = '123456789'
53+
wrapped_params = {
54+
pin_method: {
55+
option: 'SMS',
56+
},
57+
}
58+
59+
json_response = {
60+
'message' => 'sent secured Pin',
61+
}
62+
63+
allow(client).to receive(:make_request).with(
64+
:post,
65+
"fedex_registrations/#{fedex_account_number}/pin",
66+
wrapped_params,
67+
).and_return(json_response)
68+
69+
response = client.fedex_registration.request_pin(fedex_account_number, 'SMS')
70+
71+
expect(response).to be_an_instance_of(EasyPost::Models::EasyPostObject)
72+
expect(response.message).to eq('sent secured Pin')
73+
end
74+
end
75+
76+
describe '.validate_pin' do
77+
it 'validates a pin' do
78+
fedex_account_number = '123456789'
79+
pin_validation = {
80+
pin_code: '123456',
81+
name: 'BILLING NAME',
82+
}
83+
easypost_details = {
84+
carrier_account_id: 'ca_123',
85+
}
86+
params = {
87+
pin_validation: pin_validation,
88+
easypost_details: easypost_details,
89+
}
90+
91+
json_response = {
92+
'id' => 'ca_123',
93+
'object' => 'CarrierAccount',
94+
'type' => 'FedexAccount',
95+
'credentials' => {
96+
'account_number' => '123456789',
97+
'mfa_key' => '123456789-XXXXX',
98+
},
99+
}
100+
101+
allow(client).to receive(:make_request).with(
102+
:post,
103+
"fedex_registrations/#{fedex_account_number}/pin/validate",
104+
params,
105+
).and_return(json_response)
106+
107+
response = client.fedex_registration.validate_pin(fedex_account_number, params)
108+
109+
expect(response).to be_an_instance_of(EasyPost::Models::EasyPostObject)
110+
expect(response.id).to eq('ca_123')
111+
expect(response.object).to eq('CarrierAccount')
112+
expect(response.type).to eq('FedexAccount')
113+
expect(response.credentials['account_number']).to eq('123456789')
114+
expect(response.credentials['mfa_key']).to eq('123456789-XXXXX')
115+
end
116+
end
117+
118+
describe '.submit_invoice' do
119+
it 'submits details about an invoice' do
120+
fedex_account_number = '123456789'
121+
invoice_validation = {
122+
name: 'BILLING NAME',
123+
invoice_number: 'INV-12345',
124+
invoice_date: '2025-12-08',
125+
invoice_amount: '100.00',
126+
invoice_currency: 'USD',
127+
}
128+
easypost_details = {
129+
carrier_account_id: 'ca_123',
130+
}
131+
params = {
132+
invoice_validation: invoice_validation,
133+
easypost_details: easypost_details,
134+
}
135+
136+
json_response = {
137+
'id' => 'ca_123',
138+
'object' => 'CarrierAccount',
139+
'type' => 'FedexAccount',
140+
'credentials' => {
141+
'account_number' => '123456789',
142+
'mfa_key' => '123456789-XXXXX',
143+
},
144+
}
145+
146+
allow(client).to receive(:make_request).with(
147+
:post,
148+
"fedex_registrations/#{fedex_account_number}/invoice",
149+
params,
150+
).and_return(json_response)
151+
152+
response = client.fedex_registration.submit_invoice(fedex_account_number, params)
153+
154+
expect(response).to be_an_instance_of(EasyPost::Models::EasyPostObject)
155+
expect(response.id).to eq('ca_123')
156+
expect(response.object).to eq('CarrierAccount')
157+
expect(response.type).to eq('FedexAccount')
158+
expect(response.credentials['account_number']).to eq('123456789')
159+
expect(response.credentials['mfa_key']).to eq('123456789-XXXXX')
160+
end
161+
end
162+
end

0 commit comments

Comments
 (0)