diff --git a/ShippingRates/Models/FedEx/FedExErrorResponse.cs b/ShippingRates/Models/FedEx/FedExErrorResponse.cs new file mode 100644 index 0000000..2643b16 --- /dev/null +++ b/ShippingRates/Models/FedEx/FedExErrorResponse.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace ShippingRates.Models.FedEx +{ + class FedExErrorResponse + { + [JsonPropertyName("errors")] + public FedExErrorItem[] Errors { get; set; } + } + + class FedExErrorItem + { + [JsonPropertyName("code")] + public string Code { get; set; } + [JsonPropertyName("message")] + public string Message { get; set; } + } +} diff --git a/ShippingRates/Services/FedEx/FedExOAuthService.cs b/ShippingRates/Services/FedEx/FedExOAuthService.cs new file mode 100644 index 0000000..53d496b --- /dev/null +++ b/ShippingRates/Services/FedEx/FedExOAuthService.cs @@ -0,0 +1,77 @@ +using ShippingRates.ShippingProviders; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace ShippingRates.Services.FedEx +{ + internal class FedExOAuthService + { + static string GetOAuthRequestUri(bool isProduction) + => $"https://{(isProduction ? "apis" : "apis-sandbox")}.fedex.com/oauth/token"; + + public static async Task<string> GetTokenAsync(FedExProviderConfiguration configuration, HttpClient httpClient, Action<Error> reportError) + { + var token = TokenCacheService.GetToken(configuration.ClientId); + if (!string.IsNullOrEmpty(token)) + return token; + + var requestMessage = new HttpRequestMessage(HttpMethod.Post, GetOAuthRequestUri(configuration.UseProduction)); + var postData = new List<KeyValuePair<string, string>> + { + new KeyValuePair<string, string>("grant_type", "client_credentials"), + new KeyValuePair<string, string>("client_id", configuration.ClientId), + new KeyValuePair<string, string>("client_secret", configuration.ClientSecret) + }; + requestMessage.Content = new FormUrlEncodedContent(postData); + + var responseMessage = await httpClient.SendAsync(requestMessage); + var response = await responseMessage.Content.ReadAsStringAsync(); + + if (responseMessage.IsSuccessStatusCode) + { + var result = JsonSerializer.Deserialize<FedExOAuthResponse>(response); + + TokenCacheService.AddToken(configuration.ClientId, result.AccessToken, result.ExpiresIn); + + return result.AccessToken; + } + else + { + var errorResponse = JsonSerializer.Deserialize<Models.FedEx.FedExErrorResponse>(response); + if ((errorResponse?.Errors?.Length ?? 0) > 0) + { + foreach (var error in errorResponse.Errors) + { + reportError(new Error() + { + Number = error.Code, + Description = error.Message + }); + } + } + else + { + reportError(new Error() { Description = $"Unknown error while fetching FedEx OAuth token: {responseMessage.StatusCode} {response}" }); + } + + return null; + } + } + + class FedExOAuthResponse + { + [JsonPropertyName("token_type")] + public string TokenType { get; set; } + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + [JsonPropertyName("scope")] + public string Scope { get; set; } + } + } +} diff --git a/ShippingRates/Services/TokenCacheService.cs b/ShippingRates/Services/TokenCacheService.cs index 882b506..d9501c4 100644 --- a/ShippingRates/Services/TokenCacheService.cs +++ b/ShippingRates/Services/TokenCacheService.cs @@ -4,7 +4,7 @@ namespace ShippingRates.Services { /// <summary> - /// Token caching for UPS + /// Token caching for UPS, FedEx /// </summary> internal class TokenCacheService { @@ -12,7 +12,7 @@ internal class TokenCacheService /// <summary> /// Get token for a given client ID /// </summary> - /// <param name="clientId">UPS Client ID</param> + /// <param name="clientId">Client ID</param> /// <returns>Token string or null</returns> public static string GetToken(string clientId) { @@ -29,7 +29,7 @@ public static string GetToken(string clientId) /// <summary> /// Add token /// </summary> - /// <param name="clientId">UPS Client ID</param> + /// <param name="clientId">Client ID</param> /// <param name="token">Token</param> /// <param name="expiresIn">Expiration interval in seconds</param> public static void AddToken(string clientId, string token, int expiresIn) diff --git a/ShippingRates/ShippingProviders/FedExProviderConfiguration.cs b/ShippingRates/ShippingProviders/FedExProviderConfiguration.cs index a502086..db400f7 100644 --- a/ShippingRates/ShippingProviders/FedExProviderConfiguration.cs +++ b/ShippingRates/ShippingProviders/FedExProviderConfiguration.cs @@ -8,6 +8,9 @@ namespace ShippingRates.ShippingProviders { public class FedExProviderConfiguration { + /// <summary> + /// FedEx Account Number + /// </summary> public string AccountNumber { get; set; } public string Key { get; set; } public string MeterNumber { get; set; } @@ -19,5 +22,13 @@ public class FedExProviderConfiguration /// If not using the production Rate API, you can use 5531 as the HubID per FedEx documentation. /// </summary> public string HubId { get; set; } + /// <summary> + /// FedEx Client Id (required for REST API) + /// </summary> + public string ClientId { get; set; } + /// <summary> + /// FedEx Client Secret (required for REST API) + /// </summary> + public string ClientSecret { get; set; } } }