From 863f3e3d5856c71d2f13db6423abe977de528c2e Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Thu, 24 Sep 2026 09:59:35 -0600 Subject: [PATCH] chore: remove deprecated add credit card function Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 6 + .../ReferralCustomerServiceTest.cs | 242 +----------------- .../retrieve_easypost_stripe_api_key.json | 47 ++++ EasyPost/Services/ReferralCustomerService.cs | 151 +---------- 4 files changed, 66 insertions(+), 380 deletions(-) create mode 100644 EasyPost.Tests/cassettes/referral_customer_service/retrieve_easypost_stripe_api_key.json diff --git a/CHANGELOG.md b/CHANGELOG.md index b8393723..13c25d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## Next Release + +- Removes the deprecated, unusable `ReferralCustomer.AddCreditCardToUser` function + - Stripe has disabled the ability to pass plain credit card details over the wire and now requires using [Stripe.js/Elements/Checkout](https://support.stripe.com/questions/card-tokenization-restrictions-using-publishable-keys). Follow the [Decentralized (EasyPost-Manage Billing) Guide](https://docs.easypost.com/guides/get-started-with-forge/easypost-managed-billing-guide#referralcustomer-billing-management) for more details on the new flow to use. + - Makes `ReferralCustomer.RetrieveEasypostStripeApiKey` public to help facilitate adding credit cards using Stripe.js + ## v7.10.0 (2026-07-24) - Adds missing `Eccn`, `Manufacturer`, and `PrintedCommodityIdentifier` properties to the `CustomsItem` API model diff --git a/EasyPost.Tests/ServicesTests/ReferralCustomerServiceTest.cs b/EasyPost.Tests/ServicesTests/ReferralCustomerServiceTest.cs index ff9feca2..db529d5c 100644 --- a/EasyPost.Tests/ServicesTests/ReferralCustomerServiceTest.cs +++ b/EasyPost.Tests/ServicesTests/ReferralCustomerServiceTest.cs @@ -1,13 +1,9 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.Net; using System.Threading; using System.Threading.Tasks; -using EasyPost.Exceptions; using EasyPost.Exceptions.API; using EasyPost.Exceptions.General; -using EasyPost.Http; using EasyPost.Models.API; using EasyPost.Tests._Utilities; using EasyPost.Tests._Utilities.Attributes; @@ -84,213 +80,6 @@ public async Task TestGetNextPage() } } - [Fact] - [CrudOperations.Update] - [Testing.Function] - public async Task TestAddCreditCardToUser() - { - UseMockClient(new List - { - new( - new TestUtils.MockRequestMatchRules(Method.Get, @"v2\/partners\/stripe_public_key$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, content: "{\"public_key\":\"pk_test_12345\"}") - ), - new( - new TestUtils.MockRequestMatchRules(Method.Post, @"https://api.stripe.com/v1/tokens$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, content: "{\"id\":\"tok_12345\"}") - ), - new( - new TestUtils.MockRequestMatchRules(Method.Post, @"v2\/credit_cards$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, data: new PaymentMethod - { - Id = "summary_123", - Last4 = ((string)Fixtures.CreditCardDetails["number"]).Substring(12), - }) - ) - }); - - CreditCard card = new CreditCard(Fixtures.CreditCardDetails); - - PaymentMethod paymentMethod = await Client.ReferralCustomer.AddCreditCardToUser(ReferralCustomerKey, card.Number, card.ExpirationMonth, card.ExpirationYear, card.Cvc, PaymentMethod.Priority.Primary); - - Assert.NotNull(paymentMethod); - Assert.IsType(paymentMethod); - Assert.NotNull(paymentMethod.Id); - Assert.EndsWith(paymentMethod.Last4, card.Number); - - // Assert that the original API key was restored to the client properly after the request - Assert.Equal(TestUtils.GetApiKey(TestUtils.ApiKey.Mock), Client.ApiKeyInUse); - } - - [Fact] - [Testing.Parameters] - public async Task TestAddCreditCardToUserDifferentPaymentMethodPriorities() - { - UseMockClient(new List - { - new( - new TestUtils.MockRequestMatchRules(Method.Get, @"v2\/partners\/stripe_public_key$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, content: "{\"public_key\":\"pk_test_12345\"}") - ), - new( - new TestUtils.MockRequestMatchRules(Method.Post, @"https://api.stripe.com/v1/tokens$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, content: "{\"id\":\"tok_12345\"}") - ), - new( - new TestUtils.MockRequestMatchRules(Method.Post, @"v2\/credit_cards$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, data: new PaymentMethod - { - Id = "summary_123", - Last4 = ((string)Fixtures.CreditCardDetails["number"]).Substring(12), - }) - ) - }); - - CreditCard card = new CreditCard(Fixtures.CreditCardDetails); - - PaymentMethod.Priority? priority = PaymentMethod.Priority.Primary; - - PaymentMethod paymentMethod = await Client.ReferralCustomer.AddCreditCardToUser(ReferralCustomerKey, card.Number, card.ExpirationMonth, card.ExpirationYear, card.Cvc, priority); - Assert.NotNull(paymentMethod); - // If we've gotten here, no internal errors occurred on the method. - - // Test with other priorities. - priority = PaymentMethod.Priority.Secondary; - paymentMethod = await Client.ReferralCustomer.AddCreditCardToUser(ReferralCustomerKey, card.Number, card.ExpirationMonth, card.ExpirationYear, card.Cvc, priority); - Assert.NotNull(paymentMethod); - - priority = null; // Should internally default to primary priority if not specified. - paymentMethod = await Client.ReferralCustomer.AddCreditCardToUser(ReferralCustomerKey, card.Number, card.ExpirationMonth, card.ExpirationYear, card.Cvc, priority); - Assert.NotNull(paymentMethod); - } - - [Fact] - [Testing.Exception] - public async Task TestAddCreditCardToUserNoPublicKey() - { - UseMockClient(new List - { - { - new( - new TestUtils.MockRequestMatchRules(Method.Get, @"v2\/partners\/stripe_public_key$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, content: "{\"public_key\":\"\"}") - ) - } - }); - - CreditCard card = new CreditCard(Fixtures.CreditCardDetails); - - await Assert.ThrowsAsync(async () => await Client.ReferralCustomer.AddCreditCardToUser(ReferralCustomerKey, card.Number, card.ExpirationMonth, card.ExpirationYear, card.Cvc, PaymentMethod.Priority.Primary)); - } - - [Fact] - [Testing.Exception] - public async Task TestAddCreditCardToUserBadPublicKeyResponse() - { - UseMockClient(new List - { - new( - new TestUtils.MockRequestMatchRules(Method.Get, @"v2\/partners\/stripe_public_key$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, content: "{\"not_public_key\":\"random\"}") - ), - }); - - CreditCard card = new CreditCard(Fixtures.CreditCardDetails); - - await Assert.ThrowsAsync(async () => await Client.ReferralCustomer.AddCreditCardToUser(ReferralCustomerKey, card.Number, card.ExpirationMonth, card.ExpirationYear, card.Cvc, PaymentMethod.Priority.Primary)); - } - - [Fact] - [Testing.Exception] - public async Task TestAddCreditCardToUserNoStripeToken() - { - UseMockClient(new List - { - new( - new TestUtils.MockRequestMatchRules(Method.Get, @"v2\/partners\/stripe_public_key$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, content: "{\"public_key\":\"pk_test_12345\"}") - ), - new( - new TestUtils.MockRequestMatchRules(Method.Post, @"https://api.stripe.com/v1/tokens$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, content: "{\"id\":\"\"}") - ) - }); - - CreditCard card = new CreditCard(Fixtures.CreditCardDetails); - - await Assert.ThrowsAsync(async () => await Client.ReferralCustomer.AddCreditCardToUser(ReferralCustomerKey, card.Number, card.ExpirationMonth, card.ExpirationYear, card.Cvc, PaymentMethod.Priority.Primary)); - } - - [Fact] - [Testing.Exception] - public async Task TestAddCreditCardToUserStripeCantConnect() - { - UseMockClient(new List - { - new( - new TestUtils.MockRequestMatchRules(Method.Get, @"v2\/partners\/stripe_public_key$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, content: "{\"public_key\":\"pk_test_12345\"}") - ), - new( - new TestUtils.MockRequestMatchRules(Method.Post, @"https://api.stripe.com/v1/tokens$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.NotFound, content: "{}") - ) - }); - - CreditCard card = new CreditCard(Fixtures.CreditCardDetails); - - await Assert.ThrowsAsync(async () => await Client.ReferralCustomer.AddCreditCardToUser(ReferralCustomerKey, card.Number, card.ExpirationMonth, card.ExpirationYear, card.Cvc, PaymentMethod.Priority.Primary)); - } - - [Fact] - [Testing.Exception] - public async Task TestAddCreditCardToUserStripeBadResponse() - { - UseMockClient(new List - { - new( - new TestUtils.MockRequestMatchRules(Method.Get, @"v2\/partners\/stripe_public_key$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, content: "{\"public_key\":\"pk_test_12345\"}") - ), - new( - new TestUtils.MockRequestMatchRules(Method.Post, @"https://api.stripe.com/v1/tokens$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, content: "{\"not_id\":\"random\"}") - ) - }); - - CreditCard card = new CreditCard(Fixtures.CreditCardDetails); - - await Assert.ThrowsAsync(async () => await Client.ReferralCustomer.AddCreditCardToUser(ReferralCustomerKey, card.Number, card.ExpirationMonth, card.ExpirationYear, card.Cvc, PaymentMethod.Priority.Primary)); - } - - [Fact] - [Testing.Exception] - public async Task TestAddCreditCardToUserEasyPostApiError() - { - UseMockClient(new List - { - new( - new TestUtils.MockRequestMatchRules(Method.Get, @"v2\/partners\/stripe_public_key$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, content: "{\"public_key\":\"pk_test_12345\"}") - ), - new( - new TestUtils.MockRequestMatchRules(Method.Post, @"https://api.stripe.com/v1/tokens$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.OK, content: "{\"id\":\"tok_12345\"}") - ), - new( - new TestUtils.MockRequestMatchRules(Method.Post, @"v2\/credit_cards$"), - new TestUtils.MockRequestResponseInfo(HttpStatusCode.NotFound, content: "{}") - ) - }); - - CreditCard card = new CreditCard(Fixtures.CreditCardDetails); - - await Assert.ThrowsAsync(async () => await Client.ReferralCustomer.AddCreditCardToUser(ReferralCustomerKey, card.Number, card.ExpirationMonth, card.ExpirationYear, card.Cvc, PaymentMethod.Priority.Primary)); - - // Assert that the original API key was restored to the client properly even after the failed request - Assert.Equal(TestUtils.GetApiKey(TestUtils.ApiKey.Mock), Client.ApiKeyInUse); - } - [Fact] [CrudOperations.Update] [Testing.Function] @@ -350,29 +139,22 @@ await Client.ReferralCustomer.AddBankAccountFromStripe( ); } - #endregion - - #endregion - - private static string ReferralCustomerKey => TestUtils.GetApiKey(TestUtils.ApiKey.Referral); - - private sealed class CreditCard + [Fact] + [Testing.Function] + public async Task TestRetrieveEasypostStripeApiKey() { - internal readonly string Number; + UseVCR("retrieve_easypost_stripe_api_key"); - internal readonly int ExpirationMonth; + string? publicKey = await Client.ReferralCustomer.RetrieveEasypostStripeApiKey(); - internal readonly int ExpirationYear; + Assert.IsType(publicKey); + Assert.StartsWith("pk_", publicKey); + } - internal readonly string Cvc; + #endregion - internal CreditCard(IReadOnlyDictionary details) - { - Number = (string)details["number"]; - ExpirationMonth = int.Parse((string)details["expiration_month"], NumberStyles.Number, CultureInfo.InvariantCulture); - ExpirationYear = int.Parse((string)details["expiration_year"], NumberStyles.Number, CultureInfo.InvariantCulture); - Cvc = (string)details["cvc"]; - } - } + #endregion + + private static string ReferralCustomerKey => TestUtils.GetApiKey(TestUtils.ApiKey.Referral); } } diff --git a/EasyPost.Tests/cassettes/referral_customer_service/retrieve_easypost_stripe_api_key.json b/EasyPost.Tests/cassettes/referral_customer_service/retrieve_easypost_stripe_api_key.json new file mode 100644 index 00000000..0215b6ec --- /dev/null +++ b/EasyPost.Tests/cassettes/referral_customer_service/retrieve_easypost_stripe_api_key.json @@ -0,0 +1,47 @@ +[ + { + "Duration": 30, + "RecordedAt": "2026-09-24T09:43:21.000000-06:00", + "Request": { + "Body": "", + "BodyContentType": "Text", + "ContentHeaders": {}, + "Method": "GET", + "RequestHeaders": { + "Authorization": "", + "User-Agent": "" + }, + "Uri": "https://api.easypost.com/v2/partners/stripe_public_key" + }, + "Response": { + "Body": "{\"public_key\": \"pk_x3JSr5eOVWNTLRej8cZDde9VQ0AT5\"}", + "BodyContentType": "Json", + "ContentHeaders": { + "Content-Type": "application/json; charset=utf-8", + "Content-Length": "49", + "Expires": "0" + }, + "HttpVersion": "1.1", + "ResponseHeaders": { + "Cache-Control": "private, no-cache, no-store", + "Pragma": "no-cache", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", + "x-backend": "easypost", + "x-content-type-options": "nosniff", + "x-download-options": "noopen", + "x-ep-request-uuid": "91d0a0fa6ab4301ce2b97bb400af2fd1", + "x-frame-options": "SAMEORIGIN", + "x-node": "web103azw", + "x-permitted-cross-domain-policies": "none", + "x-proxied": "intlb3azw f29267e751,extlb2azw 07fb4d8f06", + "x-runtime": "0.029468", + "x-version-label": "easypost-202609231716-41b3efb4ec-main", + "x-xss-protection": "1; mode=block" + }, + "Status": { + "Code": 200, + "Message": "OK" + } + } + } +] diff --git a/EasyPost/Services/ReferralCustomerService.cs b/EasyPost/Services/ReferralCustomerService.cs index 7ebf8870..d3534534 100644 --- a/EasyPost/Services/ReferralCustomerService.cs +++ b/EasyPost/Services/ReferralCustomerService.cs @@ -1,16 +1,11 @@ -using System; using System.Collections.Generic; -using System.Globalization; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; using EasyPost._base; -using EasyPost.Exceptions; using EasyPost.Exceptions.API; using EasyPost.Exceptions.General; using EasyPost.Http; using EasyPost.Models.API; -using EasyPost.Utilities.Internal; using EasyPost.Utilities.Internal.Attributes; using EasyPost.Utilities.Internal.Extensions; @@ -106,44 +101,6 @@ public async Task All(Parameters.ReferralCustomer.Al [CrudOperations.Read] public async Task GetNextPage(ReferralCustomerCollection collection, int? pageSize = null, CancellationToken cancellationToken = default) => await collection.GetNextPage(async parameters => await All(parameters, cancellationToken), collection.ReferralCustomers, pageSize); - /// - /// Add a credit card to a . - /// This function should be called against a configured with the white label partner's API key. - /// This function requires the target 's API key as a parameter. - /// Related API documentation. - /// - /// API key of the . - /// Credit card number. - /// Expiration month of the credit card. - /// Expiration year of the credit card. - /// CVC of the credit card. - /// Priority of the credit card. - /// to use for the HTTP request. - /// The that was added. - /// When the request fails. - [CrudOperations.Update] - public async Task AddCreditCardToUser(string referralApiKey, string number, int expirationMonth, int expirationYear, string cvc, PaymentMethod.Priority? priority = null, CancellationToken cancellationToken = default) - { - string? easypostStripeApiKey = await RetrieveEasypostStripeApiKey(cancellationToken); - - if (string.IsNullOrEmpty(easypostStripeApiKey)) - { - throw new InternalServerError("Could not retrieve EasyPost Stripe API key.", 0); - } - - // ReSharper disable once RedundantSuppressNullableWarningExpression - string stripeToken = await CreateStripeToken(number, expirationMonth, expirationYear, cvc, easypostStripeApiKey, cancellationToken); - -#pragma warning disable IDE0046 - if (string.IsNullOrEmpty(stripeToken)) -#pragma warning restore IDE0046 - { - throw new ExternalApiError("Could not create Stripe token, please try again later.", 0); - } - - return await CreateEasypostCreditCard(referralApiKey, stripeToken, priority ?? PaymentMethod.Priority.Primary, cancellationToken); - } - /// /// Add a credit card to EasyPost for a with a payment method ID from Stripe. /// This function requires the 's API key. @@ -259,118 +216,12 @@ public async Task UpdateReferralEmail(string referralId, string email, Cancellat #endregion - /// - /// Submit Stripe credit card token to EasyPost for a specified . - /// - /// API key of the . - /// Stripe token. - /// Credit card priority. - /// to use for the HTTP request. - /// The that was created. - private async Task CreateEasypostCreditCard(string referralApiKey, string stripeObjectId, PaymentMethod.Priority priority, CancellationToken cancellationToken = default) - { - Dictionary parameters = new() - { - { - "credit_card", - new Dictionary - { - { "stripe_object_id", stripeObjectId }, - { "priority", priority.ToString().ToLowerInvariant() }, - } - }, - }; - - // store the original client's API key - string originalApiKey = Client.ApiKeyInUse; - - // set the client's API key to the referral customer's API key - Client.ApiKeyInUse = referralApiKey; - - PaymentMethod paymentMethod; - try - { - // Make request - paymentMethod = await Client.RequestAsync(Method.Post, "credit_cards", ApiVersion.Current, cancellationToken, parameters); - } - finally - { - // reset the client's API key to the original API key - Client.ApiKeyInUse = originalApiKey; - } - - return paymentMethod; - } - - /// - /// Create a credit card token via Stripe. - /// Related API documentation. - /// - /// Credit card number. - /// Expiration month of the credit card. - /// Expiration year of the credit card. - /// CVC of the credit card. - /// EasyPost Stripe API key. - /// to use for the HTTP request. - /// Stripe token. - /// When the request fails. - private async Task CreateStripeToken(string number, int expirationMonth, int expirationYear, string cvc, string easypostStripeApiKey, CancellationToken cancellationToken = default) - { - const string url = "https://api.stripe.com/v1/tokens"; - - HttpRequestMessage request = new(Method.Post.HttpMethod, url); - - Dictionary headers = new Dictionary - { - { "Authorization", $"Bearer {easypostStripeApiKey}" }, - { "Accept", "application/x-www-form-urlencoded" }, - }; - - foreach (KeyValuePair header in headers) - { - request.Headers.Add(header.Key, header.Value); - } - - // add parameters -#pragma warning disable SA0001 // Nullability -#pragma warning disable CS8620 // Nullability - Dictionary parameters = new Dictionary - { - { "card[number]", number }, - { "card[exp_month]", expirationMonth.ToString(CultureInfo.InvariantCulture) }, - { "card[exp_year]", expirationYear.ToString(CultureInfo.InvariantCulture) }, - { "card[cvc]", cvc }, - }; - request.Content = new FormUrlEncodedContent(parameters); -#pragma warning restore SA0001 // Nullability -#pragma warning restore CS8620 // Nullability - - HttpResponseMessage response = await Client.ExecuteRequest(request, cancellationToken); - - if (response.ReturnedError()) - { - throw new ExternalApiError("Could not send card details to Stripe, please try again later.", (int)response.StatusCode); - } - -#if NETSTANDARD2_0 || NETCOREAPP3_1 - string content = await response.Content.ReadAsStringAsync(); -#else - string content = await response.Content.ReadAsStringAsync(cancellationToken); -#endif - Dictionary data = JsonSerialization.ConvertJsonToObject>(content); - - data.TryGetValue("id", out object? id); - return id == null - ? throw new ExternalApiError("Could not send card details to Stripe, please try again later.", (int)response.StatusCode) - : (string)id; - } - /// /// Retrieve EasyPost Stripe API key. /// /// to use for the HTTP request. /// EasyPost Stripe API key. - private async Task RetrieveEasypostStripeApiKey(CancellationToken cancellationToken = default) + public async Task RetrieveEasypostStripeApiKey(CancellationToken cancellationToken = default) { Dictionary response = await RequestAsync>(Method.Get, "partners/stripe_public_key", cancellationToken);