Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Handle webhooks for ACH microdeposits lifecycle #1116

Merged
merged 14 commits into from
Feb 4, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
Stripe-Version:
- 2024-12-18.acacia
User-Agent:
- Stripe/v1 PythonBindings/11.4.1
X-Stripe-Client-User-Agent:
- '{"bindings_version": "11.4.1", "lang": "python", "publisher": "stripe", "httplib":
"requests", "lang_version": "3.12.8", "platform": "Linux-6.10.14-linuxkit-aarch64-with-glibc2.36",
"uname": "Linux f69fe8d5c257 6.10.14-linuxkit #1 SMP Fri Nov 29 17:22:03 UTC
2024 aarch64 "}'
method: GET
uri: https://api.stripe.com/v1/subscriptions/djfos?expand%5B0%5D=latest_invoice&expand%5B1%5D=customer&expand%5B2%5D=customer.invoice_settings.default_payment_method&expand%5B3%5D=customer.tax_ids
response:
body:
string: "{\n \"error\": {\n \"code\": \"resource_missing\",\n \"doc_url\":
\"https://stripe.com/docs/error-codes/resource-missing\",\n \"message\":
\"No such subscription: 'djfos'\",\n \"param\": \"id\",\n \"request_log_url\":
\"https://dashboard.stripe.com/test/logs/req_fCLmExiHliLLAy?t=1738274612\",\n
\ \"type\": \"invalid_request_error\"\n }\n}\n"
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Methods:
- GET, HEAD, PUT, PATCH, POST, DELETE
Access-Control-Allow-Origin:
- '*'
Access-Control-Expose-Headers:
- Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required,
X-Stripe-Privileged-Session-Required
Access-Control-Max-Age:
- '300'
Cache-Control:
- no-cache, no-store
Connection:
- keep-alive
Content-Length:
- '324'
Content-Security-Policy:
- base-uri 'none'; default-src 'none'; form-action 'none'; frame-ancestors 'none';
img-src 'self'; script-src 'self' 'report-sample'; style-src 'self'; upgrade-insecure-requests;
report-uri https://q.stripe.com/csp-violation?q=JU_aZLssk7a3_VZEeiDM3UWQN0mgWJiEG8zz5aFpDfoiI4Itt-XeW-vHYyCYd8ZJIklaArUO0YdslYml
Content-Type:
- application/json
Cross-Origin-Opener-Policy-Report-Only:
- same-origin; report-to="coop"
Date:
- Thu, 30 Jan 2025 22:03:32 GMT
Report-To:
- '{"group":"coop","max_age":8640,"endpoints":[{"url":"https://q.stripe.com/coop-report"}],"include_subdomains":true}'
Reporting-Endpoints:
- coop="https://q.stripe.com/coop-report"
Request-Id:
- req_fCLmExiHliLLAy
Server:
- nginx
Strict-Transport-Security:
- max-age=63072000; includeSubDomains; preload
Stripe-Version:
- 2024-12-18.acacia
Vary:
- Origin
X-Content-Type-Options:
- nosniff
X-Stripe-Priority-Routing-Enabled:
- 'true'
X-Stripe-Routing-Context-Priority-Tier:
- api-testmode
X-Wc:
- AB
status:
code: 404
message: Not Found
version: 1
53 changes: 44 additions & 9 deletions api/internal/tests/views/test_account_viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,42 @@


class MockSubscription(object):
def __init__(self, subscription_params):
def __init__(self, subscription_params: dict):
self.items = {"data": [{"id": "abc"}]}
self.cancel_at_period_end = False
self.current_period_end = 1633512445
self.latest_invoice = subscription_params["latest_invoice"]
self.latest_invoice = subscription_params.get(
"latest_invoice",
{
"id": "in_123",
"status": "complete",
},
)

default_payment_method = {
"id": "pm_123",
"card": {
"brand": "visa",
"exp_month": 12,
"exp_year": 2024,
"last4": "abcd",
},
}
self.customer = {
"invoice_settings": {
"default_payment_method": subscription_params["default_payment_method"]
"default_payment_method": subscription_params.get(
"default_payment_method", default_payment_method
)
},
"id": "cus_LK&*Hli8YLIO",
"discount": None,
"email": None,
}
self.schedule = subscription_params["schedule_id"]
self.collection_method = subscription_params["collection_method"]
self.schedule = subscription_params.get("schedule_id")
self.status = subscription_params.get("status", "active")
self.collection_method = subscription_params.get(
"collection_method", "charge_automatically"
)
self.trial_end = subscription_params.get("trial_end")

customer_coupon = subscription_params.get("customer_coupon")
Expand Down Expand Up @@ -1104,6 +1125,7 @@ def test_update_payment_method_without_body(self):
response = self.client.patch(url, format="json")
assert response.status_code == status.HTTP_400_BAD_REQUEST

@patch("services.billing.StripeService._is_unverified_payment_method")
@patch("services.billing.stripe.Subscription.retrieve")
@patch("services.billing.stripe.PaymentMethod.attach")
@patch("services.billing.stripe.Customer.modify")
Expand All @@ -1114,12 +1136,15 @@ def test_update_payment_method(
modify_customer_mock,
attach_payment_mock,
retrieve_subscription_mock,
is_unverified_payment_method_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")

is_unverified_payment_method_mock.return_value = False

default_payment_method = {
"card": {
"brand": "visa",
Expand Down Expand Up @@ -1435,14 +1460,15 @@ def test_update_can_change_name_and_email(self):
assert self.current_owner.name == expected_name
assert self.current_owner.email == expected_email

@patch("services.billing.stripe.Subscription.retrieve")
@patch("services.billing.StripeService.modify_subscription")
def test_update_handles_stripe_error(self, modify_sub_mock):
def test_update_handles_stripe_error(self, retrieve_sub_mock, modify_sub_mock):
code, message = 402, "Not right, wrong in fact"
desired_plan = {"value": PlanName.CODECOV_PRO_MONTHLY.value, "quantity": 12}
self.current_owner.stripe_customer_id = "flsoe"
self.current_owner.stripe_subscription_id = "djfos"
self.current_owner.save()

retrieve_sub_mock.return_value = MockSubscription({})
modify_sub_mock.side_effect = StripeError(message=message, http_status=code)

response = self._update(
Expand All @@ -1456,9 +1482,12 @@ def test_update_handles_stripe_error(self, modify_sub_mock):
assert response.status_code == code
assert response.data["detail"] == message

@patch("services.billing.stripe.Subscription.retrieve")
@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):
def test_update_sentry_plan_monthly(
self, modify_sub_mock, send_sentry_webhook, retrieve_sub_mock
):
desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 12}
self.current_owner.stripe_customer_id = "flsoe"
self.current_owner.stripe_subscription_id = "djfos"
Expand Down Expand Up @@ -1499,9 +1528,15 @@ def test_update_sentry_plan_monthly_with_users_org(
)
send_sentry_webhook.assert_called_once_with(self.current_owner, org)

@patch("services.billing.stripe.Subscription.retrieve")
@patch("api.internal.owner.serializers.send_sentry_webhook")
@patch("services.billing.StripeService.modify_subscription")
def test_update_sentry_plan_annual(self, modify_sub_mock, send_sentry_webhook):
def test_update_sentry_plan_annual(
self,
modify_sub_mock,
send_sentry_webhook,
retrieve_sub_mock,
):
desired_plan = {"value": PlanName.SENTRY_YEARLY.value, "quantity": 12}
self.current_owner.stripe_customer_id = "flsoe"
self.current_owner.stripe_subscription_id = "djfos"
Expand Down
2 changes: 2 additions & 0 deletions billing/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class StripeWebhookEvents:
"customer.updated",
"invoice.payment_failed",
"invoice.payment_succeeded",
"payment_intent.succeeded",
"setup_intent.succeeded",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO - these need to be turned on in Stripe dashboard

"subscription_schedule.created",
"subscription_schedule.released",
"subscription_schedule.updated",
Expand Down
Loading
Loading