From 320579d36c2b07f3c93febd0532ce8bf09700dfb Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Thu, 24 Sep 2026 09:57:08 -0600 Subject: [PATCH 1/2] chore: remove deprecated, unused addCreditCard function Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 6 + src/services/referral_customer_service.ts | 146 ++--------------- .../recording.har | 153 ++++++++++++++++++ test/services/referral_customer.test.ts | 22 +-- 4 files changed, 178 insertions(+), 149 deletions(-) create mode 100644 test/cassettes/ReferralCustomer-Service_3380152635/retrieves-EasyPost-Stripe-API-key_548125664/recording.har diff --git a/CHANGELOG.md b/CHANGELOG.md index 776ade4ce..f1949099e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## Next Release + +- Removes the deprecated, unusable `addCreditCard` 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 + ## v9.0.0-rc.1 (2026-09-08) - Breaking: Node 18+ is now required (built-in `fetch`) diff --git a/src/services/referral_customer_service.ts b/src/services/referral_customer_service.ts index c5ef0ce5c..685d2db3e 100644 --- a/src/services/referral_customer_service.ts +++ b/src/services/referral_customer_service.ts @@ -1,8 +1,4 @@ -import util from 'util'; - -import Constants from '../constants'; import EasyPostClient from '../easypost'; -import ExternalApiError from '../errors/api/external_api_error'; import User from '../models/user'; import baseService from './base_service'; import type { PaymentMethodObject } from './billing_service'; @@ -23,7 +19,6 @@ type ReferralCreateParameters = Record & { type MandateData = Record; type ReferralCustomerListResponse = { referral_customers: User[]; has_more: boolean }; type ReferralScopedClient = Pick; -type EasyPostHttpClient = Pick; /** * Get an instance of the EasyPostClient using the referral user's API key. @@ -38,96 +33,6 @@ function _getReferralClient(client: EasyPostClient, referralApiKey: string): Eas }); } -/** - * Get EasyPost's Stripe API key used to create credit cards on Stripe's servers. - * @private - * @param {EasyPostClient} easypostClient - The EasyPostClient to use. - * @returns {string} - The Stripe API key. - */ -async function _getEasyPostStripeKey(easypostClient: EasyPostHttpClient): Promise { - const url = 'partners/stripe_public_key'; - - const response = await easypostClient._get(url); - - const body = response.body as { public_key: string }; - return body.public_key; -} - -/** - * Send the credit card details to Stripe to get a Stripe credit card token. - * @private - * @param {string} stripeKey - The Stripe API key. - * @param {string} number - Credit card number. - * @param {string} expirationMonth - Credit card expiration month. - * @param {string} expirationYear - Credit card expiration year. - * @param {string} cvc - Credit card CVC. - * @returns {Promise} - Stripe credit card token. - */ -async function _sendCardDetailsToStripe( - stripeKey: string, - number: string, - expirationMonth: string, - expirationYear: string, - cvc: string, -): Promise { - const searchParams = new URLSearchParams({ - 'card[number]': number, - 'card[exp_month]': expirationMonth, - 'card[exp_year]': expirationYear, - 'card[cvc]': cvc, - }); - const url = `https://api.stripe.com/v1/tokens?${searchParams.toString()}`; - - try { - const response = await fetch(url, { - method: 'POST', - headers: { - Authorization: `Bearer ${stripeKey}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }); - - if (!response.ok) { - throw new Error('Failed Stripe request'); - } - - const body = await response.json(); - - return body.id as string; - } catch (error) { - throw new ExternalApiError({ - message: util.format(Constants.EXTERNAL_API_CALL_FAILED, 'Stripe'), - code: undefined, - statusCode: undefined, - errors: undefined, - }); - } -} - -/** - * Send the Stripe credit card token to EasyPost to add the card to the user's account. - * @private - * @param {EasyPostClient} client - The EasyPostClient to use. - * @param {string} referralApiKey - The referral user's production API key. - * @param {string} stripeCreditCardToken - Stripe credit card token. - * @param {string} priority - Whether to add the card as the 'primary' or 'secondary' card. - * @returns {Object} - Response body (EasyPost payment method object). - */ -async function _sendCardDetailsToEasyPost( - client: EasyPostClient, - referralApiKey: string, - stripeCreditCardToken: string, - priority: string, -): Promise { - const _client = _getReferralClient(client, referralApiKey); - const url = 'credit_cards'; - const params = { credit_card: { stripe_object_id: stripeCreditCardToken, priority } }; - - const response = await (_client as ReferralScopedClient)._post(url, params); - - return response.body; -} - export default (easypostClient: EasyPostClient) => /** * The ReferralCustomerService class provides methods for interacting with EasyPost {@link User referral customer} objects. @@ -166,45 +71,6 @@ export default (easypostClient: EasyPostClient) => return true; } - /** - * Add a credit card to EasyPost for a ReferralCustomer without needing a Stripe account. This function requires the ReferralCustomer User's API key. - * See {@link https://docs.easypost.com/docs/users/billing#create-credit-card EasyPost API Documentation} for more information. - * @param {string} referralApiKey - The referral customer's production API key. - * @param {string} number - The credit card number. - * @param {string} expirationMonth - The credit card expiration month. - * @param {string} expirationYear - The credit card expiration year. - * @param {string} cvc - The credit card CVC. - * @param {string} priority - Whether to add the card as 'primary' or 'secondary' payment method (defaults to 'primary'). - * @returns {Object} - An object representing the newly-added credit card. - */ - static async addCreditCard( - referralApiKey: string, - number: string, - expirationMonth: string, - expirationYear: string, - cvc: string, - priority: string = 'primary', - ): Promise { - const stripeKey = await _getEasyPostStripeKey(easypostClient); // will throw if there's an error - - const stripeCreditCardId = await _sendCardDetailsToStripe( - stripeKey, - number, - expirationMonth, - expirationYear, - cvc, - ); // will throw if there's an error - - const paymentMethod = await _sendCardDetailsToEasyPost( - easypostClient, - referralApiKey, - stripeCreditCardId, - priority, - ); // will throw if there's an error - - return paymentMethod; - } - /** * Add a credit card to EasyPost for a ReferralCustomer with a payment method ID from Stripe. * This function requires the ReferralCustomer User's API key. @@ -229,6 +95,18 @@ export default (easypostClient: EasyPostClient) => return this._convertToEasyPostObject(response.body, params); } + /** + * Retrieve EasyPost's Stripe public API key. + * @returns {string} - The Stripe API key. + */ + static async retrieveEasyPostStripeApiKey(): Promise { + const url = 'partners/stripe_public_key'; + const response = await easypostClient._get(url); + const body = response.body as { public_key: string }; + + return body.public_key; + } + /** * Add a bank account to EasyPost for a ReferralCustomer. * This function requires the ReferralCustomer User's API key. diff --git a/test/cassettes/ReferralCustomer-Service_3380152635/retrieves-EasyPost-Stripe-API-key_548125664/recording.har b/test/cassettes/ReferralCustomer-Service_3380152635/retrieves-EasyPost-Stripe-API-key_548125664/recording.har new file mode 100644 index 000000000..897e871be --- /dev/null +++ b/test/cassettes/ReferralCustomer-Service_3380152635/retrieves-EasyPost-Stripe-API-key_548125664/recording.har @@ -0,0 +1,153 @@ +{ + "log": { + "_recordingName": "ReferralCustomer Service/retrieves EasyPost Stripe API key", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "74dcf9e88cc6af64e4261fe9786d644c", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "accept", + "value": "application/json" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate" + }, + { + "name": "host", + "value": "api.easypost.com" + } + ], + "headersSize": 392, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://api.easypost.com/v2/partners/stripe_public_key" + }, + "response": { + "bodySize": 49, + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 49, + "text": "{\"public_key\":\"pk_x3JSr5eOVWNTLRej8cZDde9VQ0AT5\"}" + }, + "cookies": [], + "headers": [ + { + "name": "cache-control", + "value": "private, no-cache, no-store" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "easypost-api-version", + "value": "2015-01-01" + }, + { + "name": "expires", + "value": "0" + }, + { + "name": "pragma", + "value": "no-cache" + }, + { + "name": "referrer-policy", + "value": "strict-origin-when-cross-origin" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "vary", + "value": "EasyPost-Api-Version" + }, + { + "name": "x-backend", + "value": "easypost" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "x-download-options", + "value": "noopen" + }, + { + "name": "x-ep-request-uuid", + "value": "33fcc37b6ab54836e2b8f77a01413260" + }, + { + "name": "x-frame-options", + "value": "SAMEORIGIN" + }, + { + "name": "x-node", + "value": "web125azw" + }, + { + "name": "x-permitted-cross-domain-policies", + "value": "none" + }, + { + "name": "x-proxied", + "value": "intlb4azw f29267e751, extlb2azw 07fb4d8f06" + }, + { + "name": "x-runtime", + "value": "0.027571" + }, + { + "name": "x-version-label", + "value": "easypost-202609241501-949351e54f-main" + }, + { + "name": "x-xss-protection", + "value": "1; mode=block" + } + ], + "headersSize": 721, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2026-09-24T15:56:38.450Z", + "time": 275, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 275 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/test/services/referral_customer.test.ts b/test/services/referral_customer.test.ts index 169d1510c..be7fb0719 100644 --- a/test/services/referral_customer.test.ts +++ b/test/services/referral_customer.test.ts @@ -94,21 +94,6 @@ describe('ReferralCustomer Service', function () { ); }); - it('add a referral user credit card', async function () { - const creditCardDetails = Fixture.creditCardDetails(); - - const paymentMethod = await client.ReferralCustomer.addCreditCard( - referralUserProdApiKey, - creditCardDetails.number, - creditCardDetails.expiration_month, - creditCardDetails.expiration_year, - creditCardDetails.cvc, - ); - - expect(paymentMethod.id).to.match(/^pm_/); - expect(paymentMethod.last4).to.equal('6170'); - }); - it('raises an error when adding a credit card from Stripe fails', async function () { const billing = Fixture.billing() as ReferralCustomerBillingInput; @@ -135,4 +120,11 @@ describe('ReferralCustomer Service', function () { ); }); }); + + it('retrieves EasyPost Stripe API key', async function () { + const publicKey = await client.ReferralCustomer.retrieveEasyPostStripeApiKey(); + + expect(publicKey).to.be.a('string'); + expect(publicKey).to.match(/^pk_/); + }); }); From 065e96da763860133ed7ef28e40d57e35c6d8a04 Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Thu, 24 Sep 2026 10:02:36 -0600 Subject: [PATCH 2/2] test: remove deprecated addCreditCard cassette Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../recording.har | 472 ------------------ 1 file changed, 472 deletions(-) delete mode 100644 test/cassettes/ReferralCustomer-Service_3380152635/add-a-referral-user-credit-card_1978527022/recording.har diff --git a/test/cassettes/ReferralCustomer-Service_3380152635/add-a-referral-user-credit-card_1978527022/recording.har b/test/cassettes/ReferralCustomer-Service_3380152635/add-a-referral-user-credit-card_1978527022/recording.har deleted file mode 100644 index cbb33fd85..000000000 --- a/test/cassettes/ReferralCustomer-Service_3380152635/add-a-referral-user-credit-card_1978527022/recording.har +++ /dev/null @@ -1,472 +0,0 @@ -{ - "log": { - "_recordingName": "ReferralCustomer Service/add a referral user credit card", - "creator": { - "comment": "persister:fs", - "name": "Polly.JS", - "version": "6.0.5" - }, - "entries": [ - { - "_id": "74dcf9e88cc6af64e4261fe9786d644c", - "_order": 0, - "cache": {}, - "request": { - "bodySize": 0, - "cookies": [], - "headers": [ - { - "name": "accept-encoding", - "value": "gzip, deflate" - }, - { - "name": "accept", - "value": "application/json" - }, - { - "name": "content-type", - "value": "application/json" - }, - { - "name": "host", - "value": "api.easypost.com" - } - ], - "headersSize": 386, - "httpVersion": "HTTP/1.1", - "method": "GET", - "queryString": [], - "url": "https://api.easypost.com/v2/partners/stripe_public_key" - }, - "response": { - "bodySize": 104, - "content": { - "encoding": "base64", - "mimeType": "application/json; charset=utf-8", - "size": 104, - "text": "{\"public_key\":\"pk_x3JSr5eOVWNTLRej8cZDde9VQ0AT5\"}" - }, - "cookies": [], - "headers": [ - { - "name": "x-frame-options", - "value": "SAMEORIGIN" - }, - { - "name": "x-xss-protection", - "value": "1; mode=block" - }, - { - "name": "x-content-type-options", - "value": "nosniff" - }, - { - "name": "x-download-options", - "value": "noopen" - }, - { - "name": "x-permitted-cross-domain-policies", - "value": "none" - }, - { - "name": "referrer-policy", - "value": "strict-origin-when-cross-origin" - }, - { - "name": "x-ep-request-uuid", - "value": "6107a17366a910c7e2b87b500035512e" - }, - { - "name": "cache-control", - "value": "private, no-cache, no-store" - }, - { - "name": "pragma", - "value": "no-cache" - }, - { - "name": "expires", - "value": "0" - }, - { - "name": "content-type", - "value": "application/json; charset=utf-8" - }, - { - "name": "x-runtime", - "value": "0.023531" - }, - { - "name": "content-encoding", - "value": "gzip" - }, - { - "name": "transfer-encoding", - "value": "chunked" - }, - { - "name": "x-node", - "value": "bigweb35nuq" - }, - { - "name": "x-version-label", - "value": "easypost-202407300015-6e288fe720-master" - }, - { - "name": "x-backend", - "value": "easypost" - }, - { - "name": "x-proxied", - "value": "intlb4nuq c0f5e722d1, extlb2nuq fa152d4755" - }, - { - "name": "strict-transport-security", - "value": "max-age=31536000; includeSubDomains; preload" - }, - { - "name": "connection", - "value": "close" - } - ], - "headersSize": 710, - "httpVersion": "HTTP/1.1", - "redirectURL": "", - "status": 200, - "statusText": "OK" - }, - "startedDateTime": "2024-07-30T16:11:50.923Z", - "time": 124, - "timings": { - "blocked": -1, - "connect": -1, - "dns": -1, - "receive": 0, - "send": 0, - "ssl": -1, - "wait": 124 - } - }, - { - "_id": "9acb6c7c7e45fb72f853826cbecbbe29", - "_order": 0, - "cache": {}, - "request": { - "bodySize": 0, - "cookies": [], - "headers": [ - { - "name": "accept-encoding", - "value": "gzip, deflate" - }, - { - "name": "content-type", - "value": "application/x-www-form-urlencoded" - }, - { - "name": "host", - "value": "api.stripe.com" - } - ], - "headersSize": 308, - "httpVersion": "HTTP/1.1", - "method": "POST", - "queryString": [ - { - "name": "card", - "value": { - "cvc": "778", - "exp_month": "05", - "exp_year": "2028", - "number": "4536410136126170" - } - } - ], - "url": "https://api.stripe.com/v1/tokens?card%5Bnumber%5D=4536410136126170&card%5Bexp_month%5D=05&card%5Bexp_year%5D=2028&card%5Bcvc%5D=778" - }, - "response": { - "bodySize": 788, - "content": { - "mimeType": "application/json", - "size": 788, - "text": "{\"id\":\"tok_0PiIeRDqT4huGUvdhd7DywHH\",\"object\":\"token\",\"card\":{\"id\":\"card_0PiIeRDqT4huGUvdlRWvegOn\",\"object\":\"card\",\"address_city\":null,\"address_country\":null,\"address_line1\":null,\"address_line1_check\":null,\"address_line2\":null,\"address_state\":null,\"address_zip\":null,\"address_zip_check\":null,\"brand\":\"Visa\",\"country\":\"US\",\"cvc_check\":\"unchecked\",\"dynamic_last4\":null,\"exp_month\":5,\"exp_year\":2028,\"funding\":\"credit\",\"last4\":\"6170\",\"name\":null,\"networks\":{\"preferred\":null},\"tokenization_method\":null,\"wallet\":null},\"client_ip\":\"\",\"created\":1722355911,\"livemode\":true,\"type\":\"card\",\"used\":false}" - }, - "cookies": [], - "headers": [ - { - "name": "server", - "value": "nginx" - }, - { - "name": "date", - "value": "Tue, 30 Jul 2024 16:11:51 GMT" - }, - { - "name": "content-type", - "value": "application/json" - }, - { - "name": "content-length", - "value": "788" - }, - { - "name": "connection", - "value": "close" - }, - { - "name": "access-control-allow-credentials", - "value": "true" - }, - { - "name": "access-control-allow-methods", - "value": "GET,HEAD,PUT,PATCH,POST,DELETE" - }, - { - "name": "access-control-allow-origin", - "value": "*" - }, - { - "name": "access-control-expose-headers", - "value": "Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required" - }, - { - "name": "access-control-max-age", - "value": "300" - }, - { - "name": "cache-control", - "value": "no-cache, no-store" - }, - { - "name": "content-security-policy", - "value": "report-uri https://q.stripe.com/csp-report?p=v1%2Ftokens; block-all-mixed-content; default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; img-src 'self'; script-src 'self' 'report-sample'; style-src 'self'" - }, - { - "name": "cross-origin-opener-policy-report-only", - "value": "same-origin; report-to=\"coop\"" - }, - { - "name": "idempotency-key", - "value": "e3b71985-379a-4a10-ae00-673b9eda6961" - }, - { - "name": "original-request", - "value": "req_xn5rlFmgvFyM9Y" - }, - { - "name": "report-to", - "value": "{\"group\":\"coop\",\"max_age\":8640,\"endpoints\":[{\"url\":\"https://q.stripe.com/coop-report?s=payins-bapi-srv\"}],\"include_subdomains\":true}" - }, - { - "name": "reporting-endpoints", - "value": "coop=\"https://q.stripe.com/coop-report?s=payins-bapi-srv\"" - }, - { - "name": "request-id", - "value": "req_xn5rlFmgvFyM9Y" - }, - { - "name": "stripe-should-retry", - "value": "false" - }, - { - "name": "stripe-version", - "value": "2020-08-27" - }, - { - "name": "vary", - "value": "Origin" - }, - { - "name": "x-content-type-options", - "value": "nosniff" - }, - { - "name": "x-stripe-priority-routing-enabled", - "value": "true" - }, - { - "name": "x-stripe-routing-context-priority-tier", - "value": "livemode-critical" - }, - { - "name": "strict-transport-security", - "value": "max-age=63072000; includeSubDomains; preload" - } - ], - "headersSize": 1437, - "httpVersion": "HTTP/1.1", - "redirectURL": "", - "status": 200, - "statusText": "OK" - }, - "startedDateTime": "2024-07-30T16:11:51.052Z", - "time": 469, - "timings": { - "blocked": -1, - "connect": -1, - "dns": -1, - "receive": 0, - "send": 0, - "ssl": -1, - "wait": 469 - } - }, - { - "_id": "281f1d83d3c3fe4b0ca75d0dfc961986", - "_order": 0, - "cache": {}, - "request": { - "bodySize": 88, - "cookies": [], - "headers": [ - { - "name": "accept-encoding", - "value": "gzip, deflate" - }, - { - "name": "accept", - "value": "application/json" - }, - { - "name": "content-type", - "value": "application/json" - }, - { - "name": "content-length", - "value": 88 - }, - { - "name": "host", - "value": "api.easypost.com" - } - ], - "headersSize": 393, - "httpVersion": "HTTP/1.1", - "method": "POST", - "postData": { - "mimeType": "application/json", - "params": [], - "text": "{\"credit_card\":{\"stripe_object_id\":\"tok_0PiIeRDqT4huGUvdhd7DywHH\",\"priority\":\"primary\"}}" - }, - "queryString": [], - "url": "https://api.easypost.com/v2/credit_cards" - }, - "response": { - "bodySize": 240, - "content": { - "encoding": "base64", - "mimeType": "application/json; charset=utf-8", - "size": 240, - "text": "{\"id\":\"pm_0fe4cff2be514f36b7ca5ff75e81246c\",\"disabled_at\":null,\"object\":\"CreditCard\",\"name\":null,\"last4\":\"6170\",\"exp_month\":5,\"exp_year\":2028,\"brand\":\"Visa\",\"requires_mandate_collection\":false}" - }, - "cookies": [], - "headers": [ - { - "name": "x-frame-options", - "value": "SAMEORIGIN" - }, - { - "name": "x-xss-protection", - "value": "1; mode=block" - }, - { - "name": "x-content-type-options", - "value": "nosniff" - }, - { - "name": "x-download-options", - "value": "noopen" - }, - { - "name": "x-permitted-cross-domain-policies", - "value": "none" - }, - { - "name": "referrer-policy", - "value": "strict-origin-when-cross-origin" - }, - { - "name": "x-ep-request-uuid", - "value": "6107a17266a910c7e2b87b52003551b9" - }, - { - "name": "cache-control", - "value": "private, no-cache, no-store" - }, - { - "name": "pragma", - "value": "no-cache" - }, - { - "name": "expires", - "value": "0" - }, - { - "name": "content-type", - "value": "application/json; charset=utf-8" - }, - { - "name": "x-runtime", - "value": "2.638335" - }, - { - "name": "content-encoding", - "value": "gzip" - }, - { - "name": "transfer-encoding", - "value": "chunked" - }, - { - "name": "x-node", - "value": "bigweb43nuq" - }, - { - "name": "x-version-label", - "value": "easypost-202407300015-6e288fe720-master" - }, - { - "name": "x-backend", - "value": "easypost" - }, - { - "name": "x-canary", - "value": "direct" - }, - { - "name": "x-proxied", - "value": "intlb3nuq c0f5e722d1, extlb2nuq fa152d4755" - }, - { - "name": "strict-transport-security", - "value": "max-age=31536000; includeSubDomains; preload" - }, - { - "name": "connection", - "value": "close" - } - ], - "headersSize": 728, - "httpVersion": "HTTP/1.1", - "redirectURL": "", - "status": 201, - "statusText": "Created" - }, - "startedDateTime": "2024-07-30T16:11:51.527Z", - "time": 2750, - "timings": { - "blocked": -1, - "connect": -1, - "dns": -1, - "receive": 0, - "send": 0, - "ssl": -1, - "wait": 2750 - } - } - ], - "pages": [], - "version": "1.2" - } -}