codecov-api
codecov-api copied to clipboard
feat: Add ACH webhook flows
Handles ACH microdeposits delayed payment verification flow.
This PR handles the below new logical flows:
- User creates initial subscription from free to paid with ACH microdeposits as the selected payment method. In the 2 days it takes for that to be completed, what should they see in the UI?
- added
unverified_payment_methodslist toGET /account-details
- added
- User async verifies the ACH microdeposits for the initial checkout session
- added webhook listener for
payment_intent.succeeded - once verified, we set the account as the default payment method on customer, subscription, invoice_settings
- we also upgrade the plan at this point (TODO - confirm this)
- on Stripe's end the "charges_automatically" initital invoice that was pending gets successfully fulfilled
- added webhook listener for
- User async verifies the ACH microdeposits for subsequent paymentMethodUpdate
- added webhook listener for
setup_intent.succeeded - once verified, we set the account as the default payment method on customer, subscription, invoice_settings
- added webhook listener for
- The initial checkout session by Stripe has a "charges_automatically" that issues an invoice that fails when trying to use the ach microdeposits payment method.
- ignore this case in the webhook listener for stripe invoice.payment_failed that does other stuff like marking the account delinquent
- If the user abandons their initial checkout session that has a pending ACH, we delete that abandoned one before offering a way to make a new one
- added to the customer.subscription.deleted listener to ignore this case (vs. the one that wants to downgrade to free)
- adjusted the upgrade_plan logic to handle when we have this incomplete subscription
Other things of note:
- Stripe's list payment methods api does not included unverified paymentIntent / setupIntent
Tested end-to-end flows:
- Setup initial checkout session
- Card
- ACH instant
- ACH microdeposits
- Change payment method
- Card
- ACH instant
- ACH microdeposits
- Abandon ACH microdeposits
https://github.com/codecov/engineering-team/issues/2622
Codecov Report
Attention: Patch coverage is 34.40860% with 61 lines in your changes missing coverage. Please review.
Project coverage is 95.56%. Comparing base (
9f89174) to head (d163c61). Report is 3 commits behind head on main.
:white_check_mark: All tests successful. No failed tests found.
| Files with missing lines | Patch % | Lines |
|---|---|---|
| services/billing.py | 30.18% | 37 Missing :warning: |
| billing/views.py | 35.13% | 24 Missing :warning: |
Additional details and impacted files
@@ Coverage Diff @@
## main #1106 +/- ##
==========================================
- Coverage 96.09% 95.56% -0.54%
==========================================
Files 832 835 +3
Lines 19507 19687 +180
==========================================
+ Hits 18746 18813 +67
- Misses 761 874 +113
| Flag | Coverage Δ | |
|---|---|---|
| unit | 95.34% <34.40%> (-0.67%) |
:arrow_down: |
| unit-latest-uploader | 95.34% <34.40%> (-0.67%) |
:arrow_down: |
Flags with carried forward coverage won't be shown. Click here to find out more.
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
:x: 47 Tests Failed:
| Tests completed | Failed | Passed | Skipped |
|---|---|---|---|
| 2711 | 47 | 2664 | 7 |
View the top 3 failed tests by shortest run time
services/tests/test_billing.py::BillingServiceTests::test_update_plan_modifies_subscription_if_user_plan_and_subscription_existsStack Traces | 0.009s run time
self = <services.tests.test_billing.BillingServiceTests testMethod=test_update_plan_modifies_subscription_if_user_plan_and_subscription_exists> delete_subscription_mock = <MagicMock name='delete_subscription' id='139656576365488'> modify_subscription_mock = <MagicMock name='modify_subscription' id='139657646046000'> create_checkout_session_mock = <MagicMock name='create_checkout_session' id='139656970200096'> set_default_plan_data = <MagicMock name='set_default_plan_data' id='139656973899424'> @patch("shared.plan.service.PlanService.set_default_plan_data") @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") @patch("services.tests.test_billing.MockPaymentService.modify_subscription") @patch("services.tests.test_billing.MockPaymentService.delete_subscription") def test_update_plan_modifies_subscription_if_user_plan_and_subscription_exists( self, delete_subscription_mock, modify_subscription_mock, create_checkout_session_mock, set_default_plan_data, ): owner = OwnerFactory(stripe_subscription_id=10) desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 10} > self.billing_service.update_plan(owner, desired_plan) services/tests/test_billing.py:1939: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <services.billing.BillingService object at 0x7f04549fc860> owner = <Owner: Owner<github/cochranmichael>> desired_plan = {'quantity': 10, 'value': 'users-pr-inappy'} def update_plan(self, owner, desired_plan): """ Takes an owner and desired plan, and updates the owner's plan. Depending on current state, might create a stripe checkout session and return the checkout session's ID, which is a string. Otherwise returns None. """ if desired_plan["value"] in FREE_PLAN_REPRESENTATIONS: if owner.stripe_subscription_id is not None: self.payment_service.delete_subscription(owner) else: plan_service = PlanService(current_org=owner) plan_service.set_default_plan_data() elif desired_plan["value"] in PAID_PLANS: if owner.stripe_subscription_id is not None: # if the existing subscription is incomplete, clean it up and create a new checkout session > subscription = self.payment_service.get_subscription(owner) E TypeError: MockPaymentService.get_subscription() missing 1 required positional argument: 'plan' services/billing.py:891: TypeError
services/tests/test_billing.py::StripeServiceTests::test_update_payment_methodStack Traces | 0.036s run time
self = <services.tests.test_billing.StripeServiceTests testMethod=test_update_payment_method> modify_sub_mock = <MagicMock name='modify' id='139657641462480'> modify_customer_mock = <MagicMock name='modify' id='139657648730576'> attach_payment_mock = <MagicMock name='attach' id='139656613357152'> @patch("services.billing.stripe.PaymentMethod.attach") @patch("services.billing.stripe.Customer.modify") @patch("services.billing.stripe.Subscription.modify") def test_update_payment_method( self, modify_sub_mock, modify_customer_mock, attach_payment_mock ): payment_method_id = "pm_1234567" subscription_id = "sub_abc" customer_id = "cus_abc" owner = OwnerFactory( stripe_subscription_id=subscription_id, stripe_customer_id=customer_id ) > self.stripe.update_payment_method(owner, payment_method_id) services/tests/test_billing.py:1627: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ services/billing.py:33: in catch_and_raise return method(*args, **kwargs) services/billing.py:597: in update_payment_method should_set_as_default = not self._is_unverified_payment_method(payment_method) services/billing.py:549: in _is_unverified_payment_method payment_method = stripe.PaymentMethod.retrieve(payment_method_id) .../local/lib/python3.12.../site-packages/stripe/_payment_method.py:2755: in retrieve instance.refresh() .../local/lib/python3.12....../site-packages/stripe/_api_resource.py:38: in refresh return self._request_and_refresh("get", self.instance_url()) .../local/lib/python3.12....../site-packages/stripe/_api_resource.py:128: in _request_and_refresh obj = StripeObject._request( .../local/lib/python3.12.../site-packages/stripe/_stripe_object.py:406: in _request return self._requestor.request( .../local/lib/python3.12........./site-packages/stripe/_api_requestor.py:197: in request resp = requestor._interpret_response(rbody, rcode, rheaders, api_mode) .../local/lib/python3.12........./site-packages/stripe/_api_requestor.py:853: in _interpret_response self.handle_error_response( _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <stripe._api_requestor._APIRequestor object at 0x7f0486d39e50> rbody = '{\n "error": {\n "message": "Invalid API Key provided: default",\n "type": "invalid_request_error"\n }\n}\n' rcode = 401 resp = OrderedDict({'error': OrderedDict({'message': 'Invalid API Key provided: default', 'type': 'invalid_request_error'})}) rheaders = {'Server': 'nginx', 'Date': 'Thu, 30 Jan 2025 03:34:40 GMT', 'Content-Type': 'application/json', 'Content-Length': '10...e': 'Bearer realm="Stripe"', 'X-Wc': 'AB', 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload'} api_mode = 'V1' def handle_error_response( self, rbody, rcode, resp, rheaders, api_mode ) -> NoReturn: try: error_data = resp["error"] except (KeyError, TypeError): raise error.APIError( "Invalid response object from API: %r (HTTP response code " "was %d)" % (rbody, rcode), rbody, rcode, resp, ) err = None # OAuth errors are a JSON object where `error` is a string. In # contrast, in API errors, `error` is a hash with sub-keys. We use # this property to distinguish between OAuth and API errors. if isinstance(error_data, str): err = self.specific_oauth_error( rbody, rcode, resp, rheaders, error_data ) if err is None: err = ( self.specific_v2_api_error( rbody, rcode, resp, rheaders, error_data ) if api_mode == "V2" else self.specific_v1_api_error( rbody, rcode, resp, rheaders, error_data ) ) > raise err E stripe._error.AuthenticationError: Invalid API Key provided: default .../local/lib/python3.12........./site-packages/stripe/_api_requestor.py:336: AuthenticationError
api/internal/tests/views/test_account_viewset.py::AccountViewSetTests::test_update_payment_methodStack Traces | 0.041s run time
self = <MagicMock name='attach' id='139657640678304'>, args = ('pm_123',) kwargs = {'customer': 'flsoe'} msg = "Expected 'attach' to be called once. Called 0 times." def assert_called_once_with(self, /, *args, **kwargs): """assert that the mock was called exactly once and that that call was with the specified arguments.""" if not self.call_count == 1: msg = ("Expected '%s' to be called once. Called %s times.%s" % (self._mock_name or 'mock', self.call_count, self._calls_repr())) > raise AssertionError(msg) E AssertionError: Expected 'attach' to be called once. Called 0 times. .../local/lib/python3.12/unittest/mock.py:960: AssertionError During handling of the above exception, another exception occurred: self = <test_account_viewset.AccountViewSetTests testMethod=test_update_payment_method> modify_subscription_mock = <MagicMock name='modify' id='139657641605552'> modify_customer_mock = <MagicMock name='modify' id='139657645229168'> attach_payment_mock = <MagicMock name='attach' id='139657640678304'> retrieve_subscription_mock = <MagicMock name='retrieve' id='139657650848688'> @patch("services.billing.stripe.Subscription.retrieve") @patch("services.billing.stripe.PaymentMethod.attach") @patch("services.billing.stripe.Customer.modify") @patch("services.billing.stripe.Subscription.modify") def test_update_payment_method( self, modify_subscription_mock, modify_customer_mock, attach_payment_mock, retrieve_subscription_mock, ): self.current_owner.stripe_customer_id = "flsoe" self.current_owner.stripe_subscription_id = "djfos" self.current_owner.save() f = open("..../tests/samples/stripe_invoice.json") default_payment_method = { "card": { "brand": "visa", "exp_month": 12, "exp_year": 2024, "last4": "abcd", "should be": "removed", } } subscription_params = { "default_payment_method": default_payment_method, "cancel_at_period_end": False, "current_period_end": 1633512445, "latest_invoice": json.load(f)["data"][0], "schedule_id": None, "collection_method": "charge_automatically", "tax_ids": None, } retrieve_subscription_mock.return_value = MockSubscription(subscription_params) payment_method_id = "pm_123" kwargs = { "service": self.current_owner.service, "owner_username": self.current_owner.username, } data = {"payment_method": payment_method_id} url = reverse("account_details-update-payment", kwargs=kwargs) response = self.client.patch(url, data=data, format="json") assert response.status_code == status.HTTP_200_OK > attach_payment_mock.assert_called_once_with( payment_method_id, customer=self.current_owner.stripe_customer_id ) E AssertionError: Expected 'attach' to be called once. Called 0 times. .../tests/views/test_account_viewset.py:1148: AssertionError
To view more test analytics, go to the Test Analytics Dashboard :loudspeaker: Thoughts on this report? Let us know!
:x: 47 Tests Failed:
| Tests completed | Failed | Passed | Skipped |
|---|---|---|---|
| 2718 | 47 | 2665 | 6 |
View the top 3 failed tests by shortest run time
test_update_plan_modifies_subscription_if_user_plan_and_subscription_existsStack Traces | 0.009s run time
self = <services.tests.test_billing.BillingServiceTests testMethod=test_update_plan_modifies_subscription_if_user_plan_and_subscription_exists> delete_subscription_mock = <MagicMock name='delete_subscription' id='139656576365488'> modify_subscription_mock = <MagicMock name='modify_subscription' id='139657646046000'> create_checkout_session_mock = <MagicMock name='create_checkout_session' id='139656970200096'> set_default_plan_data = <MagicMock name='set_default_plan_data' id='139656973899424'> @patch("shared.plan.service.PlanService.set_default_plan_data") @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") @patch("services.tests.test_billing.MockPaymentService.modify_subscription") @patch("services.tests.test_billing.MockPaymentService.delete_subscription") def test_update_plan_modifies_subscription_if_user_plan_and_subscription_exists( self, delete_subscription_mock, modify_subscription_mock, create_checkout_session_mock, set_default_plan_data, ): owner = OwnerFactory(stripe_subscription_id=10) desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 10} > self.billing_service.update_plan(owner, desired_plan) services/tests/test_billing.py:1939: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <services.billing.BillingService object at 0x7f04549fc860> owner = <Owner: Owner<github/cochranmichael>> desired_plan = {'quantity': 10, 'value': 'users-pr-inappy'} def update_plan(self, owner, desired_plan): """ Takes an owner and desired plan, and updates the owner's plan. Depending on current state, might create a stripe checkout session and return the checkout session's ID, which is a string. Otherwise returns None. """ if desired_plan["value"] in FREE_PLAN_REPRESENTATIONS: if owner.stripe_subscription_id is not None: self.payment_service.delete_subscription(owner) else: plan_service = PlanService(current_org=owner) plan_service.set_default_plan_data() elif desired_plan["value"] in PAID_PLANS: if owner.stripe_subscription_id is not None: # if the existing subscription is incomplete, clean it up and create a new checkout session > subscription = self.payment_service.get_subscription(owner) E TypeError: MockPaymentService.get_subscription() missing 1 required positional argument: 'plan' services/billing.py:891: TypeError
test_update_payment_methodStack Traces | 0.036s run time
self = <services.tests.test_billing.StripeServiceTests testMethod=test_update_payment_method> modify_sub_mock = <MagicMock name='modify' id='139657641462480'> modify_customer_mock = <MagicMock name='modify' id='139657648730576'> attach_payment_mock = <MagicMock name='attach' id='139656613357152'> @patch("services.billing.stripe.PaymentMethod.attach") @patch("services.billing.stripe.Customer.modify") @patch("services.billing.stripe.Subscription.modify") def test_update_payment_method( self, modify_sub_mock, modify_customer_mock, attach_payment_mock ): payment_method_id = "pm_1234567" subscription_id = "sub_abc" customer_id = "cus_abc" owner = OwnerFactory( stripe_subscription_id=subscription_id, stripe_customer_id=customer_id ) > self.stripe.update_payment_method(owner, payment_method_id) services/tests/test_billing.py:1627: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ services/billing.py:33: in catch_and_raise return method(*args, **kwargs) services/billing.py:597: in update_payment_method should_set_as_default = not self._is_unverified_payment_method(payment_method) services/billing.py:549: in _is_unverified_payment_method payment_method = stripe.PaymentMethod.retrieve(payment_method_id) /usr/local/lib/python3.12/site-packages/stripe/_payment_method.py:2755: in retrieve instance.refresh() /usr/local/lib/python3.12/site-packages/stripe/_api_resource.py:38: in refresh return self._request_and_refresh("get", self.instance_url()) /usr/local/lib/python3.12/site-packages/stripe/_api_resource.py:128: in _request_and_refresh obj = StripeObject._request( /usr/local/lib/python3.12/site-packages/stripe/_stripe_object.py:406: in _request return self._requestor.request( /usr/local/lib/python3.12/site-packages/stripe/_api_requestor.py:197: in request resp = requestor._interpret_response(rbody, rcode, rheaders, api_mode) /usr/local/lib/python3.12/site-packages/stripe/_api_requestor.py:853: in _interpret_response self.handle_error_response( _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <stripe._api_requestor._APIRequestor object at 0x7f0486d39e50> rbody = '{\n "error": {\n "message": "Invalid API Key provided: default",\n "type": "invalid_request_error"\n }\n}\n' rcode = 401 resp = OrderedDict({'error': OrderedDict({'message': 'Invalid API Key provided: default', 'type': 'invalid_request_error'})}) rheaders = {'Server': 'nginx', 'Date': 'Thu, 30 Jan 2025 03:34:40 GMT', 'Content-Type': 'application/json', 'Content-Length': '10...e': 'Bearer realm="Stripe"', 'X-Wc': 'AB', 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload'} api_mode = 'V1' def handle_error_response( self, rbody, rcode, resp, rheaders, api_mode ) -> NoReturn: try: error_data = resp["error"] except (KeyError, TypeError): raise error.APIError( "Invalid response object from API: %r (HTTP response code " "was %d)" % (rbody, rcode), rbody, rcode, resp, ) err = None # OAuth errors are a JSON object where `error` is a string. In # contrast, in API errors, `error` is a hash with sub-keys. We use # this property to distinguish between OAuth and API errors. if isinstance(error_data, str): err = self.specific_oauth_error( rbody, rcode, resp, rheaders, error_data ) if err is None: err = ( self.specific_v2_api_error( rbody, rcode, resp, rheaders, error_data ) if api_mode == "V2" else self.specific_v1_api_error( rbody, rcode, resp, rheaders, error_data ) ) > raise err E stripe._error.AuthenticationError: Invalid API Key provided: default /usr/local/lib/python3.12/site-packages/stripe/_api_requestor.py:336: AuthenticationError
test_update_payment_methodStack Traces | 0.041s run time
self = <MagicMock name='attach' id='139657640678304'>, args = ('pm_123',) kwargs = {'customer': 'flsoe'} msg = "Expected 'attach' to be called once. Called 0 times." def assert_called_once_with(self, /, *args, **kwargs): """assert that the mock was called exactly once and that that call was with the specified arguments.""" if not self.call_count == 1: msg = ("Expected '%s' to be called once. Called %s times.%s" % (self._mock_name or 'mock', self.call_count, self._calls_repr())) > raise AssertionError(msg) E AssertionError: Expected 'attach' to be called once. Called 0 times. /usr/local/lib/python3.12/unittest/mock.py:960: AssertionError During handling of the above exception, another exception occurred: self = <test_account_viewset.AccountViewSetTests testMethod=test_update_payment_method> modify_subscription_mock = <MagicMock name='modify' id='139657641605552'> modify_customer_mock = <MagicMock name='modify' id='139657645229168'> attach_payment_mock = <MagicMock name='attach' id='139657640678304'> retrieve_subscription_mock = <MagicMock name='retrieve' id='139657650848688'> @patch("services.billing.stripe.Subscription.retrieve") @patch("services.billing.stripe.PaymentMethod.attach") @patch("services.billing.stripe.Customer.modify") @patch("services.billing.stripe.Subscription.modify") def test_update_payment_method( self, modify_subscription_mock, modify_customer_mock, attach_payment_mock, retrieve_subscription_mock, ): self.current_owner.stripe_customer_id = "flsoe" self.current_owner.stripe_subscription_id = "djfos" self.current_owner.save() f = open("./services/tests/samples/stripe_invoice.json") default_payment_method = { "card": { "brand": "visa", "exp_month": 12, "exp_year": 2024, "last4": "abcd", "should be": "removed", } } subscription_params = { "default_payment_method": default_payment_method, "cancel_at_period_end": False, "current_period_end": 1633512445, "latest_invoice": json.load(f)["data"][0], "schedule_id": None, "collection_method": "charge_automatically", "tax_ids": None, } retrieve_subscription_mock.return_value = MockSubscription(subscription_params) payment_method_id = "pm_123" kwargs = { "service": self.current_owner.service, "owner_username": self.current_owner.username, } data = {"payment_method": payment_method_id} url = reverse("account_details-update-payment", kwargs=kwargs) response = self.client.patch(url, data=data, format="json") assert response.status_code == status.HTTP_200_OK > attach_payment_mock.assert_called_once_with( payment_method_id, customer=self.current_owner.stripe_customer_id ) E AssertionError: Expected 'attach' to be called once. Called 0 times. api/internal/tests/views/test_account_viewset.py:1148: AssertionError
:mega: Thoughts on this report? Let Codecov know! | Powered by Codecov
:x: 47 Tests Failed:
| Tests completed | Failed | Passed | Skipped |
|---|---|---|---|
| 2711 | 47 | 2664 | 7 |
View the top 3 failed tests by shortest run time
services/tests/test_billing.py::BillingServiceTests::test_update_plan_modifies_subscription_if_user_plan_and_subscription_existsStack Traces | 0.009s run time
self = <services.tests.test_billing.BillingServiceTests testMethod=test_update_plan_modifies_subscription_if_user_plan_and_subscription_exists> delete_subscription_mock = <MagicMock name='delete_subscription' id='139656576365488'> modify_subscription_mock = <MagicMock name='modify_subscription' id='139657646046000'> create_checkout_session_mock = <MagicMock name='create_checkout_session' id='139656970200096'> set_default_plan_data = <MagicMock name='set_default_plan_data' id='139656973899424'> @patch("shared.plan.service.PlanService.set_default_plan_data") @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") @patch("services.tests.test_billing.MockPaymentService.modify_subscription") @patch("services.tests.test_billing.MockPaymentService.delete_subscription") def test_update_plan_modifies_subscription_if_user_plan_and_subscription_exists( self, delete_subscription_mock, modify_subscription_mock, create_checkout_session_mock, set_default_plan_data, ): owner = OwnerFactory(stripe_subscription_id=10) desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 10} > self.billing_service.update_plan(owner, desired_plan) services/tests/test_billing.py:1939: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <services.billing.BillingService object at 0x7f04549fc860> owner = <Owner: Owner<github/cochranmichael>> desired_plan = {'quantity': 10, 'value': 'users-pr-inappy'} def update_plan(self, owner, desired_plan): """ Takes an owner and desired plan, and updates the owner's plan. Depending on current state, might create a stripe checkout session and return the checkout session's ID, which is a string. Otherwise returns None. """ if desired_plan["value"] in FREE_PLAN_REPRESENTATIONS: if owner.stripe_subscription_id is not None: self.payment_service.delete_subscription(owner) else: plan_service = PlanService(current_org=owner) plan_service.set_default_plan_data() elif desired_plan["value"] in PAID_PLANS: if owner.stripe_subscription_id is not None: # if the existing subscription is incomplete, clean it up and create a new checkout session > subscription = self.payment_service.get_subscription(owner) E TypeError: MockPaymentService.get_subscription() missing 1 required positional argument: 'plan' services/billing.py:891: TypeError
services/tests/test_billing.py::StripeServiceTests::test_update_payment_methodStack Traces | 0.036s run time
self = <services.tests.test_billing.StripeServiceTests testMethod=test_update_payment_method> modify_sub_mock = <MagicMock name='modify' id='139657641462480'> modify_customer_mock = <MagicMock name='modify' id='139657648730576'> attach_payment_mock = <MagicMock name='attach' id='139656613357152'> @patch("services.billing.stripe.PaymentMethod.attach") @patch("services.billing.stripe.Customer.modify") @patch("services.billing.stripe.Subscription.modify") def test_update_payment_method( self, modify_sub_mock, modify_customer_mock, attach_payment_mock ): payment_method_id = "pm_1234567" subscription_id = "sub_abc" customer_id = "cus_abc" owner = OwnerFactory( stripe_subscription_id=subscription_id, stripe_customer_id=customer_id ) > self.stripe.update_payment_method(owner, payment_method_id) services/tests/test_billing.py:1627: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ services/billing.py:33: in catch_and_raise return method(*args, **kwargs) services/billing.py:597: in update_payment_method should_set_as_default = not self._is_unverified_payment_method(payment_method) services/billing.py:549: in _is_unverified_payment_method payment_method = stripe.PaymentMethod.retrieve(payment_method_id) .../local/lib/python3.12.../site-packages/stripe/_payment_method.py:2755: in retrieve instance.refresh() .../local/lib/python3.12....../site-packages/stripe/_api_resource.py:38: in refresh return self._request_and_refresh("get", self.instance_url()) .../local/lib/python3.12....../site-packages/stripe/_api_resource.py:128: in _request_and_refresh obj = StripeObject._request( .../local/lib/python3.12.../site-packages/stripe/_stripe_object.py:406: in _request return self._requestor.request( .../local/lib/python3.12........./site-packages/stripe/_api_requestor.py:197: in request resp = requestor._interpret_response(rbody, rcode, rheaders, api_mode) .../local/lib/python3.12........./site-packages/stripe/_api_requestor.py:853: in _interpret_response self.handle_error_response( _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <stripe._api_requestor._APIRequestor object at 0x7f0486d39e50> rbody = '{\n "error": {\n "message": "Invalid API Key provided: default",\n "type": "invalid_request_error"\n }\n}\n' rcode = 401 resp = OrderedDict({'error': OrderedDict({'message': 'Invalid API Key provided: default', 'type': 'invalid_request_error'})}) rheaders = {'Server': 'nginx', 'Date': 'Thu, 30 Jan 2025 03:34:40 GMT', 'Content-Type': 'application/json', 'Content-Length': '10...e': 'Bearer realm="Stripe"', 'X-Wc': 'AB', 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload'} api_mode = 'V1' def handle_error_response( self, rbody, rcode, resp, rheaders, api_mode ) -> NoReturn: try: error_data = resp["error"] except (KeyError, TypeError): raise error.APIError( "Invalid response object from API: %r (HTTP response code " "was %d)" % (rbody, rcode), rbody, rcode, resp, ) err = None # OAuth errors are a JSON object where `error` is a string. In # contrast, in API errors, `error` is a hash with sub-keys. We use # this property to distinguish between OAuth and API errors. if isinstance(error_data, str): err = self.specific_oauth_error( rbody, rcode, resp, rheaders, error_data ) if err is None: err = ( self.specific_v2_api_error( rbody, rcode, resp, rheaders, error_data ) if api_mode == "V2" else self.specific_v1_api_error( rbody, rcode, resp, rheaders, error_data ) ) > raise err E stripe._error.AuthenticationError: Invalid API Key provided: default .../local/lib/python3.12........./site-packages/stripe/_api_requestor.py:336: AuthenticationError
api/internal/tests/views/test_account_viewset.py::AccountViewSetTests::test_update_payment_methodStack Traces | 0.041s run time
self = <MagicMock name='attach' id='139657640678304'>, args = ('pm_123',) kwargs = {'customer': 'flsoe'} msg = "Expected 'attach' to be called once. Called 0 times." def assert_called_once_with(self, /, *args, **kwargs): """assert that the mock was called exactly once and that that call was with the specified arguments.""" if not self.call_count == 1: msg = ("Expected '%s' to be called once. Called %s times.%s" % (self._mock_name or 'mock', self.call_count, self._calls_repr())) > raise AssertionError(msg) E AssertionError: Expected 'attach' to be called once. Called 0 times. .../local/lib/python3.12/unittest/mock.py:960: AssertionError During handling of the above exception, another exception occurred: self = <test_account_viewset.AccountViewSetTests testMethod=test_update_payment_method> modify_subscription_mock = <MagicMock name='modify' id='139657641605552'> modify_customer_mock = <MagicMock name='modify' id='139657645229168'> attach_payment_mock = <MagicMock name='attach' id='139657640678304'> retrieve_subscription_mock = <MagicMock name='retrieve' id='139657650848688'> @patch("services.billing.stripe.Subscription.retrieve") @patch("services.billing.stripe.PaymentMethod.attach") @patch("services.billing.stripe.Customer.modify") @patch("services.billing.stripe.Subscription.modify") def test_update_payment_method( self, modify_subscription_mock, modify_customer_mock, attach_payment_mock, retrieve_subscription_mock, ): self.current_owner.stripe_customer_id = "flsoe" self.current_owner.stripe_subscription_id = "djfos" self.current_owner.save() f = open("..../tests/samples/stripe_invoice.json") default_payment_method = { "card": { "brand": "visa", "exp_month": 12, "exp_year": 2024, "last4": "abcd", "should be": "removed", } } subscription_params = { "default_payment_method": default_payment_method, "cancel_at_period_end": False, "current_period_end": 1633512445, "latest_invoice": json.load(f)["data"][0], "schedule_id": None, "collection_method": "charge_automatically", "tax_ids": None, } retrieve_subscription_mock.return_value = MockSubscription(subscription_params) payment_method_id = "pm_123" kwargs = { "service": self.current_owner.service, "owner_username": self.current_owner.username, } data = {"payment_method": payment_method_id} url = reverse("account_details-update-payment", kwargs=kwargs) response = self.client.patch(url, data=data, format="json") assert response.status_code == status.HTTP_200_OK > attach_payment_mock.assert_called_once_with( payment_method_id, customer=self.current_owner.stripe_customer_id ) E AssertionError: Expected 'attach' to be called once. Called 0 times. .../tests/views/test_account_viewset.py:1148: AssertionError
To view more test analytics, go to the Test Analytics Dashboard :loudspeaker: Thoughts on this report? Let us know!
:x: 47 Tests Failed:
| Tests completed | Failed | Passed | Skipped |
|---|---|---|---|
| 2712 | 47 | 2665 | 6 |
View the top 3 failed tests by shortest run time
services/tests/test_billing.py::BillingServiceTests::test_update_plan_modifies_subscription_if_user_plan_and_subscription_existsStack Traces | 0.009s run time
self = <services.tests.test_billing.BillingServiceTests testMethod=test_update_plan_modifies_subscription_if_user_plan_and_subscription_exists> delete_subscription_mock = <MagicMock name='delete_subscription' id='139656576365488'> modify_subscription_mock = <MagicMock name='modify_subscription' id='139657646046000'> create_checkout_session_mock = <MagicMock name='create_checkout_session' id='139656970200096'> set_default_plan_data = <MagicMock name='set_default_plan_data' id='139656973899424'> @patch("shared.plan.service.PlanService.set_default_plan_data") @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") @patch("services.tests.test_billing.MockPaymentService.modify_subscription") @patch("services.tests.test_billing.MockPaymentService.delete_subscription") def test_update_plan_modifies_subscription_if_user_plan_and_subscription_exists( self, delete_subscription_mock, modify_subscription_mock, create_checkout_session_mock, set_default_plan_data, ): owner = OwnerFactory(stripe_subscription_id=10) desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 10} > self.billing_service.update_plan(owner, desired_plan) services/tests/test_billing.py:1939: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <services.billing.BillingService object at 0x7f04549fc860> owner = <Owner: Owner<github/cochranmichael>> desired_plan = {'quantity': 10, 'value': 'users-pr-inappy'} def update_plan(self, owner, desired_plan): """ Takes an owner and desired plan, and updates the owner's plan. Depending on current state, might create a stripe checkout session and return the checkout session's ID, which is a string. Otherwise returns None. """ if desired_plan["value"] in FREE_PLAN_REPRESENTATIONS: if owner.stripe_subscription_id is not None: self.payment_service.delete_subscription(owner) else: plan_service = PlanService(current_org=owner) plan_service.set_default_plan_data() elif desired_plan["value"] in PAID_PLANS: if owner.stripe_subscription_id is not None: # if the existing subscription is incomplete, clean it up and create a new checkout session > subscription = self.payment_service.get_subscription(owner) E TypeError: MockPaymentService.get_subscription() missing 1 required positional argument: 'plan' services/billing.py:891: TypeError
services/tests/test_billing.py::StripeServiceTests::test_update_payment_methodStack Traces | 0.036s run time
self = <services.tests.test_billing.StripeServiceTests testMethod=test_update_payment_method> modify_sub_mock = <MagicMock name='modify' id='139657641462480'> modify_customer_mock = <MagicMock name='modify' id='139657648730576'> attach_payment_mock = <MagicMock name='attach' id='139656613357152'> @patch("services.billing.stripe.PaymentMethod.attach") @patch("services.billing.stripe.Customer.modify") @patch("services.billing.stripe.Subscription.modify") def test_update_payment_method( self, modify_sub_mock, modify_customer_mock, attach_payment_mock ): payment_method_id = "pm_1234567" subscription_id = "sub_abc" customer_id = "cus_abc" owner = OwnerFactory( stripe_subscription_id=subscription_id, stripe_customer_id=customer_id ) > self.stripe.update_payment_method(owner, payment_method_id) services/tests/test_billing.py:1627: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ services/billing.py:33: in catch_and_raise return method(*args, **kwargs) services/billing.py:597: in update_payment_method should_set_as_default = not self._is_unverified_payment_method(payment_method) services/billing.py:549: in _is_unverified_payment_method payment_method = stripe.PaymentMethod.retrieve(payment_method_id) .../local/lib/python3.12.../site-packages/stripe/_payment_method.py:2755: in retrieve instance.refresh() .../local/lib/python3.12....../site-packages/stripe/_api_resource.py:38: in refresh return self._request_and_refresh("get", self.instance_url()) .../local/lib/python3.12....../site-packages/stripe/_api_resource.py:128: in _request_and_refresh obj = StripeObject._request( .../local/lib/python3.12.../site-packages/stripe/_stripe_object.py:406: in _request return self._requestor.request( .../local/lib/python3.12........./site-packages/stripe/_api_requestor.py:197: in request resp = requestor._interpret_response(rbody, rcode, rheaders, api_mode) .../local/lib/python3.12........./site-packages/stripe/_api_requestor.py:853: in _interpret_response self.handle_error_response( _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <stripe._api_requestor._APIRequestor object at 0x7f0486d39e50> rbody = '{\n "error": {\n "message": "Invalid API Key provided: default",\n "type": "invalid_request_error"\n }\n}\n' rcode = 401 resp = OrderedDict({'error': OrderedDict({'message': 'Invalid API Key provided: default', 'type': 'invalid_request_error'})}) rheaders = {'Server': 'nginx', 'Date': 'Thu, 30 Jan 2025 03:34:40 GMT', 'Content-Type': 'application/json', 'Content-Length': '10...e': 'Bearer realm="Stripe"', 'X-Wc': 'AB', 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload'} api_mode = 'V1' def handle_error_response( self, rbody, rcode, resp, rheaders, api_mode ) -> NoReturn: try: error_data = resp["error"] except (KeyError, TypeError): raise error.APIError( "Invalid response object from API: %r (HTTP response code " "was %d)" % (rbody, rcode), rbody, rcode, resp, ) err = None # OAuth errors are a JSON object where `error` is a string. In # contrast, in API errors, `error` is a hash with sub-keys. We use # this property to distinguish between OAuth and API errors. if isinstance(error_data, str): err = self.specific_oauth_error( rbody, rcode, resp, rheaders, error_data ) if err is None: err = ( self.specific_v2_api_error( rbody, rcode, resp, rheaders, error_data ) if api_mode == "V2" else self.specific_v1_api_error( rbody, rcode, resp, rheaders, error_data ) ) > raise err E stripe._error.AuthenticationError: Invalid API Key provided: default .../local/lib/python3.12........./site-packages/stripe/_api_requestor.py:336: AuthenticationError
api/internal/tests/views/test_account_viewset.py::AccountViewSetTests::test_update_sentry_plan_monthlyStack Traces | 0.041s run time
self = <MagicMock name='send_sentry_webhook' id='139657647271152'> args = (<Owner: Owner<github/brianbrady>>, <Owner: Owner<github/brianbrady>>) kwargs = {} msg = "Expected 'send_sentry_webhook' to be called once. Called 0 times." def assert_called_once_with(self, /, *args, **kwargs): """assert that the mock was called exactly once and that that call was with the specified arguments.""" if not self.call_count == 1: msg = ("Expected '%s' to be called once. Called %s times.%s" % (self._mock_name or 'mock', self.call_count, self._calls_repr())) > raise AssertionError(msg) E AssertionError: Expected 'send_sentry_webhook' to be called once. Called 0 times. .../local/lib/python3.12/unittest/mock.py:960: AssertionError During handling of the above exception, another exception occurred: self = <test_account_viewset.AccountViewSetTests testMethod=test_update_sentry_plan_monthly> modify_sub_mock = <MagicMock name='modify_subscription' id='139657647273456'> send_sentry_webhook = <MagicMock name='send_sentry_webhook' id='139657647271152'> @patch("api.internal.owner.serializers.send_sentry_webhook") @patch("services.billing.StripeService.modify_subscription") def test_update_sentry_plan_monthly(self, modify_sub_mock, send_sentry_webhook): desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 12} self.current_owner.stripe_customer_id = "flsoe" self.current_owner.stripe_subscription_id = "djfos" self.current_owner.sentry_user_id = "sentry-user-id" self.current_owner.save() self._update( kwargs={ "service": self.current_owner.service, "owner_username": self.current_owner.username, }, data={"plan": desired_plan}, ) > send_sentry_webhook.assert_called_once_with( self.current_owner, self.current_owner ) E AssertionError: Expected 'send_sentry_webhook' to be called once. Called 0 times. .../tests/views/test_account_viewset.py:1469: AssertionError
To view more test analytics, go to the Test Analytics Dashboard :loudspeaker: Thoughts on this report? Let us know!