diff --git a/CHANGELOG.md b/CHANGELOG.md index a5b167ed..1c397829 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## Next Release + +- Adds the following functions: + - `CustomerPortal.CreateAccountLink` + - `Embeddable.CreateSession` + ## v7.3.0 (2025-11-10) - Adds support for `UspsShipAccount` diff --git a/EasyPost.Integration/TestUtils.cs b/EasyPost.Integration/TestUtils.cs index 04e0c32f..ae1204d0 100644 --- a/EasyPost.Integration/TestUtils.cs +++ b/EasyPost.Integration/TestUtils.cs @@ -12,13 +12,11 @@ public class Utils private static readonly List BodyCensors = [ - "api_keys", - "children", "client_ip", "credentials", "email", + "fields", "key", - "keys", "phone_number", "phone", "test_credentials" diff --git a/EasyPost.Tests/ServicesTests/WithParameters/CustomerPortalServiceTest.cs b/EasyPost.Tests/ServicesTests/WithParameters/CustomerPortalServiceTest.cs new file mode 100644 index 00000000..201e7800 --- /dev/null +++ b/EasyPost.Tests/ServicesTests/WithParameters/CustomerPortalServiceTest.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using EasyPost.Models.API; +using EasyPost.Parameters.CustomerPortal; +using EasyPost.Parameters.User; +using EasyPost.Tests._Utilities; +using EasyPost.Tests._Utilities.Attributes; +using EasyPost.Utilities.Internal.Attributes; +using Xunit; + +namespace EasyPost.Tests.ServicesTests.WithParameters +{ + public class CustomerPortalServiceTests : UnitTest + { + public CustomerPortalServiceTests() : base("customer_portal_service_with_parameters", TestUtils.ApiKey.Production) + { + } + + #region Tests + + #region Test CRUD Operations + + [Fact] + [CrudOperations.Read] + [Testing.Function] + public async Task TestCreateAccountLink() + { + UseVCR("create_account_link"); + + Dictionary fixture = new Dictionary { { "page_size", Fixtures.PageSize } }; + AllChildren childrenParameters = Fixtures.Parameters.Users.AllChildren(fixture); + ChildUserCollection childUserCollection = await Client.User.AllChildren(childrenParameters); + + Parameters.CustomerPortal.CreateAccountLink parameters = new() + { + SessionType = "account_onboarding", + UserId = childUserCollection.Children[0].Id, + RefreshUrl = "https://example.com/refresh", + ReturnUrl = "https://example.com/return", + }; + CustomerPortalAccountLink accountLink = await Client.CustomerPortal.CreateAccountLink(parameters); + + Assert.IsType(accountLink); + } + + #endregion + + #endregion + } +} diff --git a/EasyPost.Tests/ServicesTests/WithParameters/EmbeddableServiceTest.cs b/EasyPost.Tests/ServicesTests/WithParameters/EmbeddableServiceTest.cs new file mode 100644 index 00000000..18258c35 --- /dev/null +++ b/EasyPost.Tests/ServicesTests/WithParameters/EmbeddableServiceTest.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using EasyPost.Models.API; +using EasyPost.Parameters.Embeddable; +using EasyPost.Parameters.User; +using EasyPost.Tests._Utilities; +using EasyPost.Tests._Utilities.Attributes; +using EasyPost.Utilities.Internal.Attributes; +using Xunit; + +namespace EasyPost.Tests.ServicesTests.WithParameters +{ + public class EmbeddableServiceTests : UnitTest + { + public EmbeddableServiceTests() : base("embeddable_service_with_parameters", TestUtils.ApiKey.Production) + { + } + + #region Tests + + #region Test CRUD Operations + + [Fact] + [CrudOperations.Read] + [Testing.Function] + public async Task TestCreateSession() + { + UseVCR("create_session"); + + Dictionary fixture = new Dictionary { { "page_size", Fixtures.PageSize } }; + AllChildren childrenParameters = Fixtures.Parameters.Users.AllChildren(fixture); + ChildUserCollection childUserCollection = await Client.User.AllChildren(childrenParameters); + + Parameters.Embeddable.CreateSession parameters = new() + { + OriginHost = "https://example.com", + UserId = childUserCollection.Children[0].Id, + }; + EmbeddablesSession session = await Client.Embeddable.CreateSession(parameters); + + Assert.IsType(session); + } + + #endregion + + #endregion + } +} diff --git a/EasyPost.Tests/TestUtils.cs b/EasyPost.Tests/TestUtils.cs index dfef3eb6..32320752 100644 --- a/EasyPost.Tests/TestUtils.cs +++ b/EasyPost.Tests/TestUtils.cs @@ -23,13 +23,11 @@ public class TestUtils private static readonly List BodyCensors = [ - "api_keys", - "children", "client_ip", "credentials", "email", + "fields", "key", - "keys", "phone_number", "phone", "test_credentials" diff --git a/EasyPost.Tests/cassettes/net/customer_portal_service_with_parameters/create_account_link.json b/EasyPost.Tests/cassettes/net/customer_portal_service_with_parameters/create_account_link.json new file mode 100644 index 00000000..87a51ec9 --- /dev/null +++ b/EasyPost.Tests/cassettes/net/customer_portal_service_with_parameters/create_account_link.json @@ -0,0 +1,97 @@ +[ + { + "Duration": 355, + "RecordedAt": "2025-11-21T11:57:16.671831-07:00", + "Request": { + "Body": "", + "BodyContentType": "Text", + "ContentHeaders": {}, + "Method": "GET", + "RequestHeaders": { + "Authorization": "", + "User-Agent": "" + }, + "Uri": "https://api.easypost.com/v2/users/children?page_size=5" + }, + "Response": { + "Body": "{\"children\":[{\"id\":\"user_4233cc5e2dd543a6bf5397e9594ecb68\",\"object\":\"User\",\"parent_id\":\"user_4d78588f2f744bf6886aa67ddb870865\",\"name\":\"Child User\",\"phone_number\":\"\",\"verified\":true,\"created_at\":\"2024-02-19T07:59:59Z\"},{\"id\":\"user_8796f6de77154885ab5044feb9c03f81\",\"object\":\"User\",\"parent_id\":\"user_4d78588f2f744bf6886aa67ddb870865\",\"name\":\"Child User\",\"phone_number\":\"\",\"verified\":true,\"created_at\":\"2024-02-19T08:00:00Z\"},{\"id\":\"user_844e0c84bd5b47e59bdb46eccb0f45f3\",\"object\":\"User\",\"parent_id\":\"user_4d78588f2f744bf6886aa67ddb870865\",\"name\":\"Child User\",\"phone_number\":\"\",\"verified\":true,\"created_at\":\"2024-02-19T08:11:29Z\"},{\"id\":\"user_db23a582d10547c7ab84212304407a5e\",\"object\":\"User\",\"parent_id\":\"user_4d78588f2f744bf6886aa67ddb870865\",\"name\":\"Test User\",\"phone_number\":\"\",\"verified\":true,\"created_at\":\"2025-11-21T18:30:36Z\"}],\"has_more\":false}", + "BodyContentType": "Json", + "ContentHeaders": { + "Expires": "0", + "Content-Type": "application/json; charset=utf-8", + "Content-Length": "890" + }, + "HttpVersion": "1.1", + "ResponseHeaders": { + "X-Frame-Options": "SAMEORIGIN", + "X-XSS-Protection": "1; mode=block", + "X-Content-Type-Options": "nosniff", + "x-download-options": "noopen", + "x-permitted-cross-domain-policies": "none", + "Referrer-Policy": "strict-origin-when-cross-origin", + "x-ep-request-uuid": "12829f636920b60ce2b7d23300c47da7", + "Cache-Control": "no-store, no-cache, private", + "Pragma": "no-cache", + "x-runtime": "0.114642", + "x-node": "bigweb42nuq", + "x-version-label": "easypost-202511211422-7701450e91-master", + "x-backend": "easypost", + "x-proxied": "intlb3nuq c0061e0a2e,extlb2nuq cbbd141214", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload" + }, + "Status": { + "Code": 200, + "Message": "OK" + } + } + }, + { + "Duration": 161, + "RecordedAt": "2025-11-21T11:57:16.839838-07:00", + "Request": { + "Body": "{\"session_type\":\"account_onboarding\",\"user_id\":\"user_4233cc5e2dd543a6bf5397e9594ecb68\",\"refresh_url\":\"https://example.com/refresh\",\"return_url\":\"https://example.com/return\"}", + "BodyContentType": "Json", + "ContentHeaders": { + "Content-Type": "application/json; charset=utf-8", + "Content-Length": "173" + }, + "Method": "POST", + "RequestHeaders": { + "Authorization": "", + "User-Agent": "" + }, + "Uri": "https://api.easypost.com/v2/customer_portal/account_link" + }, + "Response": { + "Body": "{\"link\":\"https://app.easypost.com/customer-portal/onboarding?session_id=bkHWIz8zP1OY7zPM\",\"object\":\"CustomerPortalAccountLink\",\"expires_at\":\"2025-11-21T19:02:16Z\",\"created_at\":\"2025-11-21T18:57:16Z\"}", + "BodyContentType": "Json", + "ContentHeaders": { + "Expires": "0", + "Content-Type": "application/json; charset=utf-8", + "Content-Length": "199" + }, + "HttpVersion": "1.1", + "ResponseHeaders": { + "X-Frame-Options": "SAMEORIGIN", + "X-XSS-Protection": "1; mode=block", + "X-Content-Type-Options": "nosniff", + "x-download-options": "noopen", + "x-permitted-cross-domain-policies": "none", + "Referrer-Policy": "strict-origin-when-cross-origin", + "x-ep-request-uuid": "12829f636920b60ce2b7d23300c47de2", + "Cache-Control": "no-store, no-cache, private", + "Pragma": "no-cache", + "x-runtime": "0.124963", + "x-node": "bigweb64nuq", + "x-version-label": "easypost-202511211422-7701450e91-master", + "x-backend": "easypost", + "x-proxied": "intlb3nuq c0061e0a2e,extlb2nuq cbbd141214", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload" + }, + "Status": { + "Code": 201, + "Message": "Created" + } + } + } +] diff --git a/EasyPost.Tests/cassettes/net/embeddable_service_with_parameters/create_session.json b/EasyPost.Tests/cassettes/net/embeddable_service_with_parameters/create_session.json new file mode 100644 index 00000000..b7ac1565 --- /dev/null +++ b/EasyPost.Tests/cassettes/net/embeddable_service_with_parameters/create_session.json @@ -0,0 +1,97 @@ +[ + { + "Duration": 401, + "RecordedAt": "2025-11-21T11:55:40.425-07:00", + "Request": { + "Body": "", + "BodyContentType": "Text", + "ContentHeaders": {}, + "Method": "GET", + "RequestHeaders": { + "Authorization": "", + "User-Agent": "" + }, + "Uri": "https://api.easypost.com/v2/users/children?page_size=5" + }, + "Response": { + "Body": "{\"children\":[{\"id\":\"user_4233cc5e2dd543a6bf5397e9594ecb68\",\"object\":\"User\",\"parent_id\":\"user_4d78588f2f744bf6886aa67ddb870865\",\"name\":\"Child User\",\"phone_number\":\"\",\"verified\":true,\"created_at\":\"2024-02-19T07:59:59Z\"},{\"id\":\"user_8796f6de77154885ab5044feb9c03f81\",\"object\":\"User\",\"parent_id\":\"user_4d78588f2f744bf6886aa67ddb870865\",\"name\":\"Child User\",\"phone_number\":\"\",\"verified\":true,\"created_at\":\"2024-02-19T08:00:00Z\"},{\"id\":\"user_844e0c84bd5b47e59bdb46eccb0f45f3\",\"object\":\"User\",\"parent_id\":\"user_4d78588f2f744bf6886aa67ddb870865\",\"name\":\"Child User\",\"phone_number\":\"\",\"verified\":true,\"created_at\":\"2024-02-19T08:11:29Z\"},{\"id\":\"user_db23a582d10547c7ab84212304407a5e\",\"object\":\"User\",\"parent_id\":\"user_4d78588f2f744bf6886aa67ddb870865\",\"name\":\"Test User\",\"phone_number\":\"\",\"verified\":true,\"created_at\":\"2025-11-21T18:30:36Z\"}],\"has_more\":false}", + "BodyContentType": "Json", + "ContentHeaders": { + "Expires": "0", + "Content-Type": "application/json; charset=utf-8", + "Content-Length": "890" + }, + "HttpVersion": "1.1", + "ResponseHeaders": { + "X-Frame-Options": "SAMEORIGIN", + "X-XSS-Protection": "1; mode=block", + "X-Content-Type-Options": "nosniff", + "x-download-options": "noopen", + "x-permitted-cross-domain-policies": "none", + "Referrer-Policy": "strict-origin-when-cross-origin", + "x-ep-request-uuid": "40a2aa286920b5ace2b7d210040dd7d1", + "Cache-Control": "no-store, no-cache, private", + "Pragma": "no-cache", + "x-runtime": "0.082476", + "x-node": "bigweb65nuq", + "x-version-label": "easypost-202511211422-7701450e91-master", + "x-backend": "easypost", + "x-proxied": "intlb6nuq c0061e0a2e,extlb1nuq cbbd141214", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload" + }, + "Status": { + "Code": 200, + "Message": "OK" + } + } + }, + { + "Duration": 219, + "RecordedAt": "2025-11-21T11:55:40.651018-07:00", + "Request": { + "Body": "{\"origin_host\":\"https://example.com\",\"user_id\":\"user_4233cc5e2dd543a6bf5397e9594ecb68\"}", + "BodyContentType": "Json", + "ContentHeaders": { + "Content-Type": "application/json; charset=utf-8", + "Content-Length": "87" + }, + "Method": "POST", + "RequestHeaders": { + "Authorization": "", + "User-Agent": "" + }, + "Uri": "https://api.easypost.com/v2/embeddables/session" + }, + "Response": { + "Body": "{\"object\":\"EmbeddablesSession\",\"session_id\":\"CX4nUufTfiWiqgn1\",\"expires_at\":\"2025-11-21T19:10:40Z\",\"created_at\":\"2025-11-21T18:55:40Z\"}", + "BodyContentType": "Json", + "ContentHeaders": { + "Expires": "0", + "Content-Type": "application/json; charset=utf-8", + "Content-Length": "135" + }, + "HttpVersion": "1.1", + "ResponseHeaders": { + "X-Frame-Options": "SAMEORIGIN", + "X-XSS-Protection": "1; mode=block", + "X-Content-Type-Options": "nosniff", + "x-download-options": "noopen", + "x-permitted-cross-domain-policies": "none", + "Referrer-Policy": "strict-origin-when-cross-origin", + "x-ep-request-uuid": "40a2aa286920b5ace2b7d210040dd801", + "Cache-Control": "no-store, no-cache, private", + "Pragma": "no-cache", + "x-runtime": "0.182859", + "x-node": "bigweb58nuq", + "x-version-label": "easypost-202511211422-7701450e91-master", + "x-backend": "easypost", + "x-proxied": "intlb6nuq c0061e0a2e,extlb1nuq cbbd141214", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload" + }, + "Status": { + "Code": 201, + "Message": "Created" + } + } + } +] diff --git a/EasyPost/Client.cs b/EasyPost/Client.cs index a6c1a83e..449532a4 100644 --- a/EasyPost/Client.cs +++ b/EasyPost/Client.cs @@ -53,6 +53,11 @@ public class Client : EasyPostClient /// public ClaimService Claim => new ClaimService(this); + /// + /// Access CustomerPortal-related functionality. + /// + public CustomerPortalService CustomerPortal => new CustomerPortalService(this); + /// /// Access Customs Info-related functionality. /// @@ -63,6 +68,11 @@ public class Client : EasyPostClient /// public CustomsItemService CustomsItem => new CustomsItemService(this); + /// + /// Access Embeddable-related functionality. + /// + public EmbeddableService Embeddable => new EmbeddableService(this); + /// /// Access EndShipper-related functionality. /// diff --git a/EasyPost/Models/API/CustomerPortalAccountLink.cs b/EasyPost/Models/API/CustomerPortalAccountLink.cs new file mode 100644 index 00000000..adbf87d8 --- /dev/null +++ b/EasyPost/Models/API/CustomerPortalAccountLink.cs @@ -0,0 +1,33 @@ +using EasyPost._base; +using Newtonsoft.Json; + +namespace EasyPost.Models.API +{ + /// + /// Class representing a CustomerPortalAccountLink. + /// + public class CustomerPortalAccountLink : EphemeralEasyPostObject + { + #region JSON Properties + + /// + /// One-time-use session URL for initiating the Customer Portal. + /// + [JsonProperty("link")] + public string? Link { get; set; } + + /// + /// One-time-use session URL for initiating the Customer Portal. + /// + [JsonProperty("created_at")] + public string? CreatedAt { get; set; } + + /// + /// ISO 8601 timestamp when the link will expire (5 minutes from creation). + /// + [JsonProperty("expires_at")] + public string? ExpiresAt { get; set; } + + #endregion + } +} diff --git a/EasyPost/Models/API/EmbeddablesSession.cs b/EasyPost/Models/API/EmbeddablesSession.cs new file mode 100644 index 00000000..3eb4f652 --- /dev/null +++ b/EasyPost/Models/API/EmbeddablesSession.cs @@ -0,0 +1,34 @@ +using EasyPost._base; +using Newtonsoft.Json; + +namespace EasyPost.Models.API +{ + /// + /// Class representing an EmbeddablesSession. + /// + public class EmbeddablesSession : EphemeralEasyPostObject + { + #region JSON Properties + + /// + /// Short-lived, one-time-use token that authorizes an Embeddables Components session. + /// Must be provided to the client-side Embeddables script to initialize the component. + /// + [JsonProperty("session_id")] + public string? SessionId { get; set; } + + /// + /// ISO 8601 timestamp indicating when the session was created. + /// + [JsonProperty("created_at")] + public string? CreatedAt { get; set; } + + /// + /// ISO 8601 timestamp indicating when the session expires. + /// + [JsonProperty("expires_at")] + public string? ExpiresAt { get; set; } + + #endregion + } +} diff --git a/EasyPost/Parameters/CustomerPortal/CreateAccountLink.cs b/EasyPost/Parameters/CustomerPortal/CreateAccountLink.cs new file mode 100644 index 00000000..d5ee07ad --- /dev/null +++ b/EasyPost/Parameters/CustomerPortal/CreateAccountLink.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using EasyPost.Utilities.Internal.Attributes; + +namespace EasyPost.Parameters.CustomerPortal +{ + /// + /// Parameters for API calls. + /// + [ExcludeFromCodeCoverage] + public class CreateAccountLink : BaseParameters + { + #region Request Parameters + + /// + /// Type of Customer Portal session. + /// + [TopLevelRequestParameter(Necessity.Required, "session_type")] + public string? SessionType { get; set; } + + /// + /// The User ID of the sub account for which the portal session is being created. + /// + [TopLevelRequestParameter(Necessity.Required, "user_id")] + public string? UserId { get; set; } + + /// + /// The URL to which the sub account will be redirected if the session URL is expired, reused, or otherwise invalid. + /// This should trigger a new session request. + /// + [TopLevelRequestParameter(Necessity.Required, "refresh_url")] + public string? RefreshUrl { get; set; } + + /// + /// The URL to which the sub account will be redirected after exiting the Customer Portal session. + /// This does not confirm completion of the flow; webhook or API polling is recommended for confirmation. + /// + [TopLevelRequestParameter(Necessity.Required, "return_url")] + public string? ReturnUrl { get; set; } + + /// + /// Used to configure the Customer Portal session. + /// + [TopLevelRequestParameter(Necessity.Optional, "metadata")] + public Dictionary? Metadata { get; set; } + + #endregion + } +} diff --git a/EasyPost/Parameters/Embeddable/CreateSession.cs b/EasyPost/Parameters/Embeddable/CreateSession.cs new file mode 100644 index 00000000..32ca0ca7 --- /dev/null +++ b/EasyPost/Parameters/Embeddable/CreateSession.cs @@ -0,0 +1,28 @@ +using System.Diagnostics.CodeAnalysis; +using EasyPost.Utilities.Internal.Attributes; + +namespace EasyPost.Parameters.Embeddable +{ + /// + /// Parameters for API calls. + /// + [ExcludeFromCodeCoverage] + public class CreateSession : BaseParameters + { + #region Request Parameters + + /// + /// The integrator’s domain in bare-host format (e.g., example.com), excluding protocol and subdomains. + /// + [TopLevelRequestParameter(Necessity.Required, "origin_host")] + public string? OriginHost { get; set; } + + /// + /// The User ID of the sub account for which the embeddable session is being created. + /// + [TopLevelRequestParameter(Necessity.Required, "user_id")] + public string? UserId { get; set; } + + #endregion + } +} diff --git a/EasyPost/Services/CustomerPortalService.cs b/EasyPost/Services/CustomerPortalService.cs new file mode 100644 index 00000000..a25042a4 --- /dev/null +++ b/EasyPost/Services/CustomerPortalService.cs @@ -0,0 +1,38 @@ +using System.Threading; +using System.Threading.Tasks; +using EasyPost._base; +using EasyPost.Http; +using EasyPost.Models.API; +using EasyPost.Utilities.Internal.Attributes; + +namespace EasyPost.Services +{ + /// + /// Class representing a set of CustomerPortal-related functionality. + /// + // ReSharper disable once ClassNeverInstantiated.Global + public class CustomerPortalService : EasyPostService + { + /// + /// Initializes a new instance of the class. + /// + /// The to tie to this service and use for API calls. + internal CustomerPortalService(EasyPostClient client) + : base(client) + { + } + + #region CRUD Operations + + /// + /// Create a Portal Session. + /// + /// Data to use to create the Portal Session. + /// to use for the HTTP request. + /// A object. + [CrudOperations.Create] + public async Task CreateAccountLink(Parameters.CustomerPortal.CreateAccountLink parameters, CancellationToken cancellationToken = default) => await RequestAsync(Method.Post, "customer_portal/account_link", cancellationToken, parameters.ToDictionary()); + + #endregion + } +} diff --git a/EasyPost/Services/EmbeddableService.cs b/EasyPost/Services/EmbeddableService.cs new file mode 100644 index 00000000..119819d4 --- /dev/null +++ b/EasyPost/Services/EmbeddableService.cs @@ -0,0 +1,38 @@ +using System.Threading; +using System.Threading.Tasks; +using EasyPost._base; +using EasyPost.Http; +using EasyPost.Models.API; +using EasyPost.Utilities.Internal.Attributes; + +namespace EasyPost.Services +{ + /// + /// Class representing a set of Embeddable-related functionality. + /// + // ReSharper disable once ClassNeverInstantiated.Global + public class EmbeddableService : EasyPostService + { + /// + /// Initializes a new instance of the class. + /// + /// The to tie to this service and use for API calls. + internal EmbeddableService(EasyPostClient client) + : base(client) + { + } + + #region CRUD Operations + + /// + /// Create an Embeddables Session. + /// + /// Data to use to create the Embeddables Session. + /// to use for the HTTP request. + /// A object. + [CrudOperations.Create] + public async Task CreateSession(Parameters.Embeddable.CreateSession parameters, CancellationToken cancellationToken = default) => await RequestAsync(Method.Post, "embeddables/session", cancellationToken, parameters.ToDictionary()); + + #endregion + } +}