diff --git a/CHANGELOG.md b/CHANGELOG.md index e4140753..792752c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## Next Release + +- Adds the following functions to assist ReferralCustomers add credit cards and bank accounts: + - `BetaReferralCustomerService.retrieve_credit_card_client_secret` + - `BetaReferralCustomerService.retrieve_bank_account_client_secret` + - `ReferralCustomerService.add_credit_card_from_stripe` + - `ReferralCustomerService.add_bank_account_from_stripe` + ## v9.5.0 (2024-10-24) - Adds `tracking_codes` as a parameter of the `all` method on the TrackerService diff --git a/easypost/services/beta_referral_customer_service.py b/easypost/services/beta_referral_customer_service.py index 95c2df3f..2e7dab45 100644 --- a/easypost/services/beta_referral_customer_service.py +++ b/easypost/services/beta_referral_customer_service.py @@ -1,6 +1,7 @@ from typing import ( Any, Dict, + Optional, ) from easypost.easypost_object import convert_to_easypost_object @@ -25,7 +26,7 @@ def add_payment_method( EasyPost, we will associate your Stripe payment method with either your primary or secondary EasyPost payment method. """ - wrapped_params = { + params = { "payment_method": { "stripe_customer_id": stripe_customer_id, "payment_method_reference": payment_method_reference, @@ -36,7 +37,7 @@ def add_payment_method( response = Requestor(self._client).request( method=RequestMethod.POST, url="/referral_customers/payment_method", - params=wrapped_params, + params=params, beta=True, ) @@ -44,12 +45,12 @@ def add_payment_method( def refund_by_amount(self, refund_amount: int) -> Dict[str, Any]: """Refund a ReferralCustomer wallet by specifying an amount.""" - wrapped_params = {"refund_amount": refund_amount} + params = {"refund_amount": refund_amount} response = Requestor(self._client).request( method=RequestMethod.POST, url="/referral_customers/refunds", - params=wrapped_params, + params=params, beta=True, ) @@ -57,12 +58,35 @@ def refund_by_amount(self, refund_amount: int) -> Dict[str, Any]: def refund_by_payment_log(self, payment_log_id: str) -> Dict[str, Any]: """Refund a ReferralCustomer wallet by specifying a payment log ID to completely refund.""" - wrapped_params = {"payment_log_id": payment_log_id} + params = {"payment_log_id": payment_log_id} response = Requestor(self._client).request( method=RequestMethod.POST, url="/referral_customers/refunds", - params=wrapped_params, + params=params, + beta=True, + ) + + return convert_to_easypost_object(response=response) + + def retrieve_credit_card_client_secret(self) -> Dict[str, Any]: + """Retrieves a client secret to use with Stripe when adding a credit card.""" + response = Requestor(self._client).request( + method=RequestMethod.POST, + url="/setup_intents", + beta=True, + ) + + return convert_to_easypost_object(response=response) + + def retrieve_bank_account_client_secret(self, return_url: Optional[str] = None) -> Dict[str, Any]: + """Retrieves a client secret to use with Stripe when adding a bank account.""" + params = {"return_url": return_url} + + response = Requestor(self._client).request( + method=RequestMethod.POST, + url="/financial_connections_sessions", + params=params if return_url else None, beta=True, ) diff --git a/easypost/services/referral_customer_service.py b/easypost/services/referral_customer_service.py index 30fe3918..fb76350d 100644 --- a/easypost/services/referral_customer_service.py +++ b/easypost/services/referral_customer_service.py @@ -104,7 +104,7 @@ def add_credit_card( cvc: str, priority: str = "primary", ) -> Dict[str, Any]: - """Add credit card to a referral customer. + """Add a credit card to EasyPost for a ReferralCustomer without needing a Stripe account. This function requires the ReferralCustomer User's API key. """ @@ -129,6 +129,64 @@ def add_credit_card( return convert_to_easypost_object(response) + def add_credit_card_from_stripe( + self, + referral_api_key: str, + payment_method_id: str, + priority: str = "primary", + ) -> Dict[str, Any]: + """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. + """ + params = { + "credit_card": { + "payment_method_id": payment_method_id, + "priority": priority, + } + } + + # Override the API key to use the referral's for this single request + referral_client = deepcopy(self._client) + referral_client.api_key = referral_api_key + + response = Requestor(referral_client).request( + method=RequestMethod.POST, + params=params, + url="/credit_cards", + ) + + return convert_to_easypost_object(response) + + def add_bank_account_from_stripe( + self, + referral_api_key: str, + financial_connections_id: str, + mandate_data: Dict[str, Any], + priority: str = "primary", + ) -> Dict[str, Any]: + """Add a bank account to EasyPost for a ReferralCustomer. + + This function requires the ReferralCustomer User's API key. + """ + params = { + "financial_connections_id": financial_connections_id, + "mandate_data": mandate_data, + "priority": priority, + } + + # Override the API key to use the referral's for this single request + referral_client = deepcopy(self._client) + referral_client.api_key = referral_api_key + + response = Requestor(referral_client).request( + method=RequestMethod.POST, + params=params, + url="/bank_accounts", + ) + + return convert_to_easypost_object(response) + def _retrieve_easypost_stripe_api_key(self) -> str: """Retrieve EasyPost's Stripe public API key.""" public_key = Requestor(self._client).request( diff --git a/tests/cassettes/test_beta_referral_customer_retrieve_bank_account_client_secret.yaml b/tests/cassettes/test_beta_referral_customer_retrieve_bank_account_client_secret.yaml new file mode 100644 index 00000000..454e100c --- /dev/null +++ b/tests/cassettes/test_beta_referral_customer_retrieve_bank_account_client_secret.yaml @@ -0,0 +1,69 @@ +interactions: +- request: + body: '{}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '2' + Content-Type: + - application/json + authorization: + - + user-agent: + - + method: POST + uri: https://api.easypost.com/beta/financial_connections_sessions + response: + body: + string: '{"client_secret": "fcsess_client_secret_MFA1Ze7exUG6pRSEHUwlsqjF"}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '65' + content-type: + - application/json; charset=utf-8 + expires: + - '0' + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + vary: + - Origin + x-backend: + - easypost + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - 595be110677d75bbe2b7da4f0007bffa + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb35nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb3nuq 51d74985a2 + - extlb1nuq 99aac35317 + x-runtime: + - '0.468505' + x-version-label: + - easypost-202501062233-3c9302ef23-master + x-xss-protection: + - 1; mode=block + status: + code: 201 + message: Created +version: 1 diff --git a/tests/cassettes/test_beta_referral_customer_retrieve_credit_card_client_secret.yaml b/tests/cassettes/test_beta_referral_customer_retrieve_credit_card_client_secret.yaml new file mode 100644 index 00000000..9df25ca1 --- /dev/null +++ b/tests/cassettes/test_beta_referral_customer_retrieve_credit_card_client_secret.yaml @@ -0,0 +1,69 @@ +interactions: +- request: + body: '{}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '2' + Content-Type: + - application/json + authorization: + - + user-agent: + - + method: POST + uri: https://api.easypost.com/beta/setup_intents + response: + body: + string: '{"client_secret": "seti_0QehjADqT4huGUvdfjjqoH19_secret_RXnJlDWhUSWZnYSASXGZLVKiCbNWcJX"}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '88' + content-type: + - application/json; charset=utf-8 + expires: + - '0' + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + vary: + - Origin + x-backend: + - easypost + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - 327fc0a7677d7580e2b7da2e0007de98 + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb57nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb4nuq 51d74985a2 + - extlb2nuq 99aac35317 + x-runtime: + - '0.468862' + x-version-label: + - easypost-202501062233-3c9302ef23-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_referral_customer_add_bank_account_from_stripe.yaml b/tests/cassettes/test_referral_customer_add_bank_account_from_stripe.yaml new file mode 100644 index 00000000..bae7f247 --- /dev/null +++ b/tests/cassettes/test_referral_customer_add_bank_account_from_stripe.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: '{"financial_connections_id": "fca_0QAc7sDqT4huGUvdf6BahYa9", "mandate_data": + {"ip_address": "127.0.0.1", "user_agent": "Mozilla/5.0", "accepted_at": 1722510730}, + "priority": "primary"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '184' + Content-Type: + - application/json + authorization: + - + user-agent: + - + method: POST + uri: https://api.easypost.com/v2/bank_accounts + response: + body: + string: '{"error": {"code": "BANK_ACCOUNT.INVALID_PARAMS", "message": "account_holder_name + must be present when creating a Financial Connections payment method", "errors": + []}}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '161' + content-type: + - application/json; charset=utf-8 + expires: + - '0' + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-backend: + - easypost + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - 595be10f677d770ce2b7da9900093399 + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb38nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb3nuq 51d74985a2 + - extlb1nuq 99aac35317 + x-runtime: + - '0.032022' + x-version-label: + - easypost-202501062233-3c9302ef23-master + x-xss-protection: + - 1; mode=block + status: + code: 422 + message: Unprocessable Entity +version: 1 diff --git a/tests/cassettes/test_referral_customer_add_credit_card_from_stripe.yaml b/tests/cassettes/test_referral_customer_add_credit_card_from_stripe.yaml new file mode 100644 index 00000000..8e6ced6e --- /dev/null +++ b/tests/cassettes/test_referral_customer_add_credit_card_from_stripe.yaml @@ -0,0 +1,69 @@ +interactions: +- request: + body: '{"credit_card": {"payment_method_id": "pm_0Pn6bQDqT4huGUvd0CjpRerH", "priority": + "primary"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '92' + Content-Type: + - application/json + authorization: + - + user-agent: + - + method: POST + uri: https://api.easypost.com/v2/credit_cards + response: + body: + string: '{"error": {"code": "CREDIT_CARD.NOT_FOUND", "message": "Stripe::PaymentMethod + does not exist for the specified reference_id", "errors": []}}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '134' + content-type: + - application/json; charset=utf-8 + expires: + - '0' + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-backend: + - easypost + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - 327fc0ad677d76bee2b7da9200097934 + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb34nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb4nuq 51d74985a2 + - extlb2nuq 99aac35317 + x-runtime: + - '0.426801' + x-version-label: + - easypost-202501062233-3c9302ef23-master + x-xss-protection: + - 1; mode=block + status: + code: 404 + message: Not Found +version: 1 diff --git a/tests/test_beta_referral_customer.py b/tests/test_beta_referral_customer.py index 9740c1c9..63c11bd9 100644 --- a/tests/test_beta_referral_customer.py +++ b/tests/test_beta_referral_customer.py @@ -40,3 +40,19 @@ def test_beta_referral_customer_refund_by_payment_log(referral_customer_prod_cli referral_customer_prod_client.beta_referral_customer.refund_by_payment_log(payment_log_id="paylog_123") assert str(error.value) == "We could not find a transaction with that id." + + +@pytest.mark.vcr() +def test_beta_referral_customer_retrieve_credit_card_client_secret(referral_customer_prod_client): + """This test requires a referral customer's production API key via REFERRAL_CUSTOMER_PROD_API_KEY.""" + response = referral_customer_prod_client.beta_referral_customer.retrieve_credit_card_client_secret() + + assert str.startswith(response.client_secret, "seti_") + + +@pytest.mark.vcr() +def test_beta_referral_customer_retrieve_bank_account_client_secret(referral_customer_prod_client): + """This test requires a referral customer's production API key via REFERRAL_CUSTOMER_PROD_API_KEY.""" + response = referral_customer_prod_client.beta_referral_customer.retrieve_bank_account_client_secret() + + assert str.startswith(response.client_secret, "fcsess_client_secret_") diff --git a/tests/test_referral_customer.py b/tests/test_referral_customer.py index 94e1cec0..52b9b587 100644 --- a/tests/test_referral_customer.py +++ b/tests/test_referral_customer.py @@ -7,6 +7,7 @@ _TEST_FAILED_INTENTIONALLY_ERROR, NO_MORE_PAGES_ERROR, ) +from easypost.errors.api.api_error import ApiError from easypost.models import User @@ -122,3 +123,42 @@ def test_referral_add_credit_card_error( ) assert str(error.value) == "Could not send card details to Stripe, please try again later." + + +@pytest.mark.vcr() +def test_referral_customer_add_credit_card_from_stripe(partner_user_prod_client, credit_card_details): + """This test requires a referral customer's production API key via REFERRAL_CUSTOMER_PROD_API_KEY. + + We expect this test to fail because we don't have valid billing details to use. Assert the correct error. + """ + with pytest.raises(ApiError) as error: + partner_user_prod_client.referral_customer.add_credit_card_from_stripe( + referral_api_key=REFERRAL_CUSTOMER_PROD_API_KEY, + payment_method_id="pm_0Pn6bQDqT4huGUvd0CjpRerH", + priority="primary", + ) + + assert str(error.value) == "Stripe::PaymentMethod does not exist for the specified reference_id" + + +@pytest.mark.vcr() +def test_referral_customer_add_bank_account_from_stripe(partner_user_prod_client, credit_card_details): + """This test requires a referral customer's production API key via REFERRAL_CUSTOMER_PROD_API_KEY. + + We expect this test to fail because we don't have valid billing details to use. Assert the correct error. + """ + with pytest.raises(ApiError) as error: + partner_user_prod_client.referral_customer.add_bank_account_from_stripe( + referral_api_key=REFERRAL_CUSTOMER_PROD_API_KEY, + financial_connections_id="fca_0QAc7sDqT4huGUvdf6BahYa9", + mandate_data={ + "ip_address": "127.0.0.1", + "user_agent": "Mozilla/5.0", + "accepted_at": 1722510730, + }, + priority="primary", + ) + + assert ( + str(error.value) == "account_holder_name must be present when creating a Financial Connections payment method" + )