Skip to content

Commit 9f13d06

Browse files
Justintime50claude
andauthored
feat: add FedEx multi-factor authentication registration support (#649)
Ports the work from EasyPost/easypost-java#367 to here. Closes #648 --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 04331e8 commit 9f13d06

File tree

9 files changed

+557
-0
lines changed

9 files changed

+557
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
using System.Collections.Generic;
2+
using System.Net;
3+
using System.Threading.Tasks;
4+
using EasyPost.Http;
5+
using EasyPost.Models.API;
6+
using EasyPost.Tests._Utilities;
7+
using Xunit;
8+
9+
namespace EasyPost.Tests.ServicesTests
10+
{
11+
public class FedExRegistrationServiceTests : UnitTest
12+
{
13+
public FedExRegistrationServiceTests() : base("fedex_registration_service", TestUtils.ApiKey.Production)
14+
{
15+
}
16+
17+
protected override IEnumerable<TestUtils.MockRequest> MockRequests
18+
{
19+
get
20+
{
21+
return new List<TestUtils.MockRequest>
22+
{
23+
new(
24+
new TestUtils.MockRequestMatchRules(Method.Post, @"v2\/fedex_registrations\/\S*\/address$"),
25+
new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, data: new FedExAccountValidationResponse
26+
{
27+
EmailAddress = "test@example.com",
28+
PhoneNumber = "5555555555",
29+
Options = new List<string> { "SMS", "CALL", "INVOICE" },
30+
})
31+
),
32+
new(
33+
new TestUtils.MockRequestMatchRules(Method.Post, @"v2\/fedex_registrations\/\S*\/pin$"),
34+
new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, data: new FedExRequestPinResponse
35+
{
36+
Message = "Your secured PIN has been sent to your phone.",
37+
})
38+
),
39+
new(
40+
new TestUtils.MockRequestMatchRules(Method.Post, @"v2\/fedex_registrations\/\S*\/pin\/validate$"),
41+
new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, data: new FedExAccountValidationResponse
42+
{
43+
Id = "ca_test123",
44+
ObjectType = "CarrierAccount",
45+
Type = "FedexAccount",
46+
Credentials = new Dictionary<string, string>
47+
{
48+
{ "account_number", "123456789" },
49+
{ "mfa_key", "test_mfa_key" },
50+
},
51+
})
52+
),
53+
new(
54+
new TestUtils.MockRequestMatchRules(Method.Post, @"v2\/fedex_registrations\/\S*\/invoice$"),
55+
new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, data: new FedExAccountValidationResponse
56+
{
57+
Id = "ca_test123",
58+
ObjectType = "CarrierAccount",
59+
Type = "FedexAccount",
60+
Credentials = new Dictionary<string, string>
61+
{
62+
{ "account_number", "123456789" },
63+
{ "mfa_key", "test_mfa_key" },
64+
},
65+
})
66+
),
67+
};
68+
}
69+
}
70+
71+
#region Tests
72+
73+
[Fact]
74+
public async Task TestRegisterAddress()
75+
{
76+
UseMockClient();
77+
78+
Parameters.FedExRegistration.RegisterAddress parameters = new Parameters.FedExRegistration.RegisterAddress
79+
{
80+
Name = "test_name",
81+
Company = "test_company",
82+
Street1 = "test_street",
83+
City = "test_city",
84+
State = "test_state",
85+
Zip = "test_zip",
86+
Country = "US",
87+
Phone = "test_phone",
88+
};
89+
90+
FedExAccountValidationResponse response = await Client.FedExRegistration.RegisterAddress("123456789", parameters);
91+
92+
Assert.NotNull(response);
93+
Assert.NotNull(response.Options);
94+
Assert.Contains("SMS", response.Options);
95+
Assert.Contains("CALL", response.Options);
96+
Assert.Contains("INVOICE", response.Options);
97+
Assert.NotNull(response.PhoneNumber);
98+
}
99+
100+
[Fact]
101+
public async Task TestRequestPin()
102+
{
103+
UseMockClient();
104+
105+
FedExRequestPinResponse response = await Client.FedExRegistration.RequestPin("123456789", "SMS");
106+
107+
Assert.NotNull(response);
108+
Assert.NotNull(response.Message);
109+
Assert.Contains("secured PIN", response.Message);
110+
}
111+
112+
[Fact]
113+
public async Task TestValidatePin()
114+
{
115+
UseMockClient();
116+
117+
Parameters.FedExRegistration.ValidatePin parameters = new Parameters.FedExRegistration.ValidatePin
118+
{
119+
Name = "test_name",
120+
Pin = "123456",
121+
};
122+
123+
FedExAccountValidationResponse response = await Client.FedExRegistration.ValidatePin("123456789", parameters);
124+
125+
Assert.NotNull(response);
126+
Assert.NotNull(response.Credentials);
127+
Assert.True(response.Credentials.ContainsKey("account_number"));
128+
Assert.True(response.Credentials.ContainsKey("mfa_key"));
129+
}
130+
131+
[Fact]
132+
public async Task TestSubmitInvoice()
133+
{
134+
UseMockClient();
135+
136+
Parameters.FedExRegistration.SubmitInvoice parameters = new Parameters.FedExRegistration.SubmitInvoice
137+
{
138+
Name = "test_name",
139+
InvoiceNumber = "test_invoice",
140+
InvoiceAmount = "100.00",
141+
InvoiceDate = "2023-01-01",
142+
};
143+
144+
FedExAccountValidationResponse response = await Client.FedExRegistration.SubmitInvoice("123456789", parameters);
145+
146+
Assert.NotNull(response);
147+
Assert.NotNull(response.Credentials);
148+
Assert.True(response.Credentials.ContainsKey("account_number"));
149+
Assert.True(response.Credentials.ContainsKey("mfa_key"));
150+
}
151+
152+
#endregion
153+
}
154+
}

EasyPost/Client.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ public class Client : EasyPostClient
8383
/// </summary>
8484
public EventService Event => new EventService(this);
8585

86+
/// <summary>
87+
/// Access FedEx Registration-related functionality.
88+
/// </summary>
89+
public FedExRegistrationService FedExRegistration => new FedExRegistrationService(this);
90+
8691
/// <summary>
8792
/// Access Insurance-related functionality.
8893
/// </summary>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System.Collections.Generic;
2+
using EasyPost._base;
3+
using Newtonsoft.Json;
4+
5+
namespace EasyPost.Models.API
6+
{
7+
/// <summary>
8+
/// Represents a FedEx account validation response.
9+
/// </summary>
10+
public class FedExAccountValidationResponse : EphemeralEasyPostObject
11+
{
12+
#region JSON Properties
13+
14+
/// <summary>
15+
/// Gets or sets the email address for PIN delivery.
16+
/// </summary>
17+
[JsonProperty("email_address")]
18+
public string? EmailAddress { get; set; }
19+
20+
/// <summary>
21+
/// Gets or sets the available PIN delivery options.
22+
/// </summary>
23+
[JsonProperty("options")]
24+
public List<string>? Options { get; set; }
25+
26+
/// <summary>
27+
/// Gets or sets the phone number for PIN delivery.
28+
/// </summary>
29+
[JsonProperty("phone_number")]
30+
public string? PhoneNumber { get; set; }
31+
32+
/// <summary>
33+
/// Gets or sets the ID.
34+
/// </summary>
35+
[JsonProperty("id")]
36+
public string? Id { get; set; }
37+
38+
/// <summary>
39+
/// Gets or sets the object type.
40+
/// </summary>
41+
[JsonProperty("object")]
42+
public string? ObjectType { get; set; }
43+
44+
/// <summary>
45+
/// Gets or sets the type.
46+
/// </summary>
47+
[JsonProperty("type")]
48+
public string? Type { get; set; }
49+
50+
/// <summary>
51+
/// Gets or sets the credentials.
52+
/// </summary>
53+
[JsonProperty("credentials")]
54+
public Dictionary<string, string>? Credentials { get; set; }
55+
56+
#endregion
57+
}
58+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using EasyPost._base;
2+
using Newtonsoft.Json;
3+
4+
namespace EasyPost.Models.API
5+
{
6+
/// <summary>
7+
/// Represents a FedEx request PIN response.
8+
/// </summary>
9+
public class FedExRequestPinResponse : EphemeralEasyPostObject
10+
{
11+
#region JSON Properties
12+
13+
/// <summary>
14+
/// Gets or sets the message.
15+
/// </summary>
16+
[JsonProperty("message")]
17+
public string? Message { get; set; }
18+
19+
#endregion
20+
}
21+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System;
2+
using System.Diagnostics.CodeAnalysis;
3+
using EasyPost.Utilities.Internal.Attributes;
4+
5+
namespace EasyPost.Parameters.FedExRegistration
6+
{
7+
/// <summary>
8+
/// Parameters for <see cref="EasyPost.Services.FedExRegistrationService.RegisterAddress(string, RegisterAddress, System.Threading.CancellationToken)"/> API calls.
9+
/// </summary>
10+
[ExcludeFromCodeCoverage]
11+
public class RegisterAddress : BaseParameters<Models.API.FedExAccountValidationResponse>, IFedExRegistrationParameter
12+
{
13+
#region Request Parameters
14+
15+
/// <summary>
16+
/// Name for the FedEx registration.
17+
/// If not provided, a UUID will be auto-generated.
18+
/// </summary>
19+
[TopLevelRequestParameter(Necessity.Optional, "address_validation", "name")]
20+
public string? Name { get; set; }
21+
22+
/// <summary>
23+
/// Company name for the FedEx registration.
24+
/// </summary>
25+
[TopLevelRequestParameter(Necessity.Optional, "address_validation", "company")]
26+
public string? Company { get; set; }
27+
28+
/// <summary>
29+
/// First street line for the FedEx registration.
30+
/// </summary>
31+
[TopLevelRequestParameter(Necessity.Optional, "address_validation", "street1")]
32+
public string? Street1 { get; set; }
33+
34+
/// <summary>
35+
/// Second street line for the FedEx registration.
36+
/// </summary>
37+
[TopLevelRequestParameter(Necessity.Optional, "address_validation", "street2")]
38+
public string? Street2 { get; set; }
39+
40+
/// <summary>
41+
/// City for the FedEx registration.
42+
/// </summary>
43+
[TopLevelRequestParameter(Necessity.Optional, "address_validation", "city")]
44+
public string? City { get; set; }
45+
46+
/// <summary>
47+
/// State for the FedEx registration.
48+
/// </summary>
49+
[TopLevelRequestParameter(Necessity.Optional, "address_validation", "state")]
50+
public string? State { get; set; }
51+
52+
/// <summary>
53+
/// ZIP code for the FedEx registration.
54+
/// </summary>
55+
[TopLevelRequestParameter(Necessity.Optional, "address_validation", "zip")]
56+
public string? Zip { get; set; }
57+
58+
/// <summary>
59+
/// Country code for the FedEx registration.
60+
/// </summary>
61+
[TopLevelRequestParameter(Necessity.Optional, "address_validation", "country")]
62+
public string? Country { get; set; }
63+
64+
/// <summary>
65+
/// Phone number for the FedEx registration.
66+
/// </summary>
67+
[TopLevelRequestParameter(Necessity.Optional, "address_validation", "phone")]
68+
public string? Phone { get; set; }
69+
70+
/// <summary>
71+
/// Email address for the FedEx registration.
72+
/// </summary>
73+
[TopLevelRequestParameter(Necessity.Optional, "address_validation", "email")]
74+
public string? Email { get; set; }
75+
76+
/// <summary>
77+
/// Carrier account ID for the FedEx registration.
78+
/// </summary>
79+
[TopLevelRequestParameter(Necessity.Optional, "easypost_details", "carrier_account_id")]
80+
public string? CarrierAccountId { get; set; }
81+
82+
#endregion
83+
84+
/// <summary>
85+
/// Override the default <see cref="BaseParameters{TMatchInputType}.ToDictionary"/> method to ensure the "name" field exists.
86+
/// If not present, generates a UUID (with hyphens removed) as the name.
87+
/// </summary>
88+
/// <returns>A <see cref="System.Collections.Generic.Dictionary{TKey,TValue}"/>.</returns>
89+
public override System.Collections.Generic.Dictionary<string, object> ToDictionary()
90+
{
91+
if (string.IsNullOrWhiteSpace(Name))
92+
{
93+
Name = Guid.NewGuid().ToString().Replace("-", string.Empty);
94+
}
95+
96+
return base.ToDictionary();
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)