Skip to content

Commit

Permalink
feat: Handle webhooks for ACH microdeposits lifecycle (#1116)
Browse files Browse the repository at this point in the history
Co-authored-by: Ajay Singh <[email protected]>
  • Loading branch information
suejung-sentry and ajay-sentry authored Feb 4, 2025
1 parent d13cceb commit 714ba37
Show file tree
Hide file tree
Showing 8 changed files with 915 additions and 43 deletions.
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",
"subscription_schedule.created",
"subscription_schedule.released",
"subscription_schedule.updated",
Expand Down
Loading

0 comments on commit 714ba37

Please sign in to comment.